import { readFileSync } from 'node:fs'; import { join, normalize } from 'pathe'; import { parse as parseToml } from 'smol-toml'; import { type CollectionView } from '#/_base/di/collection'; import { Disposable } from '#/_base/di/lifecycle'; import { LifecycleScope } from '#/app/scopes'; import { ScopeActivation, registerScopedService } from '#/_base/di/scope'; import { Emitter, type Event } from '#/_base/event'; import { TimeoutTimer } from '#/_base/utils/timer'; import { BugIndicatingError, Error2, ErrorCodes, onUnexpectedError } from '#/errors'; import { IBootstrapService } from '#/app/bootstrap/bootstrap'; import { ILogService } from '#/_base/log/log'; import { IAtomicTomlDocumentStore } from '#/persistence/interface/atomicDocumentStore'; import { watch } from '#human/utils/watch'; import { type AnyEnvBindings, type ConfigChangedEvent, type ConfigDiagnostic, type ConfigSectionChangedEvent, type ConfigEffectiveOverlay, type ConfigInspectValue, type ConfigMerge, type ConfigOverlayRegisteredEvent, type ConfigSchema, type ConfigSection, type ConfigSectionRegisteredEvent, type ConfigChangeSource, type EnvBinding, type RegisterSectionOptions, type ResolvedConfig, ConfigScope, ConfigTarget, IConfigRegistry, IConfigService, } from './config'; import { deepEqual, deepMerge, describeUnknownError, isPlainObject } from './configPure'; import { ConfigSectionContribution, getConfigSectionContributions, } from './configSectionContributions'; import { getConfigOverlayContributions } from './configOverlayContributions'; import { collectKeyDeprecations } from './deprecations'; import { applySectionToToml, camelToSnake, cloneRecord, describeTomlSyntaxError, TomlError, transformTomlData, } from './toml'; import { planConfigWriteback } from './tomlWriteback'; const CONFIG_SCOPE = ''; const WATCH_DEBOUNCE_MS = 150; type GetEnv = (name: string) => string | undefined; type OnDeprecatedEnv = (oldName: string, newName: string) => void; function isEnvBinding(value: unknown): value is EnvBinding { return typeof value === 'string' || (isPlainObject(value) && 'env' in value); } function parseBoundRaw(binding: EnvBinding, raw: string): unknown { return typeof binding === 'string' ? raw : binding.parse ? binding.parse(raw) : raw; } function resolveBinding( binding: EnvBinding, getEnv: GetEnv, existing: unknown, onDeprecatedEnv?: OnDeprecatedEnv, ): unknown { if (typeof binding !== 'string') { const raw = getEnv(binding.env); if (raw !== undefined) { const parsed = parseBoundRaw(binding, raw); if (parsed !== undefined) return parsed; } if (binding.deprecatedEnv !== undefined) { const deprecatedRaw = getEnv(binding.deprecatedEnv); if (deprecatedRaw !== undefined) { const parsed = parseBoundRaw(binding, deprecatedRaw); if (parsed !== undefined) { onDeprecatedEnv?.(binding.deprecatedEnv, binding.env); return parsed; } } } } else { const raw = getEnv(binding); if (raw !== undefined) return raw; } if (typeof binding === 'object' && binding.default !== undefined && existing === undefined) { return binding.default; } return existing; } function applyEnvBindings( target: Record, bindings: AnyEnvBindings, getEnv: GetEnv, onDeprecatedEnv?: OnDeprecatedEnv, ): void { for (const [key, binding] of Object.entries(bindings)) { if (isEnvBinding(binding)) { const resolved = resolveBinding(binding, getEnv, target[key], onDeprecatedEnv); if (resolved !== undefined) target[key] = resolved; } else if (binding !== undefined) { const child: Record = isPlainObject(target[key]) ? { ...target[key] } : {}; target[key] = child; applyEnvBindings(child, binding as AnyEnvBindings, getEnv, onDeprecatedEnv); if (Object.keys(child).length === 0) { delete target[key]; } } } } export function applySectionEnv( base: unknown, env: AnyEnvBindings, getEnv: GetEnv, onDeprecatedEnv?: OnDeprecatedEnv, ): unknown { if (isEnvBinding(env)) { return resolveBinding(env, getEnv, base, onDeprecatedEnv); } const target: Record = isPlainObject(base) ? { ...base } : {}; applyEnvBindings(target, env, getEnv, onDeprecatedEnv); return target; } function isSameSection( existing: ConfigSection, schema: ConfigSchema, options: RegisterSectionOptions, ): boolean { return ( existing.schema === schema && existing.merge === (options.merge ?? deepMerge) && existing.scope === (options.scope ?? ConfigScope.Core) && existing.env === (options.env as ConfigSection['env']) && existing.stripEnv === (options.stripEnv as ConfigSection['stripEnv']) && existing.fromToml === options.fromToml && existing.toToml === options.toToml && deepEqual(existing.defaultValue, options.defaultValue) && deepEqual(existing.deprecations, options.deprecations) && existing.collectDiagnostics === options.collectDiagnostics ); } export class ConfigRegistry extends Disposable implements IConfigRegistry { declare readonly _serviceBrand: undefined; private readonly sections = new Map(); private readonly overlays: ConfigEffectiveOverlay[] = []; private readonly _onDidRegisterSection = this._register( new Emitter(), ); readonly onDidRegisterSection: Event = this._onDidRegisterSection.event; private readonly _onDidUnregisterSection = this._register( new Emitter(), ); readonly onDidUnregisterSection: Event = this._onDidUnregisterSection.event; private readonly _onDidRegisterOverlay = this._register( new Emitter(), ); readonly onDidRegisterOverlay: Event = this._onDidRegisterOverlay.event; private readonly foldDomains = new Set(); constructor( @ConfigSectionContribution view?: CollectionView, ) { super(); for (const c of getConfigSectionContributions()) { this.registerSection(c.domain, c.schema, c.options); } for (const overlay of getConfigOverlayContributions()) { this.registerEffectiveOverlay(overlay); } if (view === undefined) return; for (const item of view.items) { this.addContribution(item); } this._register( view.onDidChange((change) => { for (const contribution of change.removed) { this.removeContribution(contribution); } for (const contribution of change.added) { this.addContribution(contribution); } }), ); } private addContribution(contribution: ConfigSectionContribution): void { const before = this.sections.get(contribution.domain); try { this.registerSection(contribution.domain, contribution.schema, contribution.options); } catch (error) { onUnexpectedError(error); return; } if (before === undefined && this.sections.get(contribution.domain) !== undefined) { this.foldDomains.add(contribution.domain); } } private removeContribution(contribution: ConfigSectionContribution): void { if (!this.foldDomains.delete(contribution.domain)) return; this.unregisterSection(contribution.domain); } registerSection( domain: string, schema: ConfigSchema, options: RegisterSectionOptions = {}, ): void { const existing = this.sections.get(domain); if (existing !== undefined) { if ( isSameSection( existing, schema as ConfigSchema, options as RegisterSectionOptions, ) ) { return; } throw new BugIndicatingError(`ConfigRegistry: section '${domain}' is already registered`); } this.sections.set(domain, { domain, schema: schema as ConfigSchema, defaultValue: options.defaultValue, merge: (options.merge ?? deepMerge) as ConfigMerge, scope: options.scope ?? ConfigScope.Core, env: options.env as ConfigSection['env'], stripEnv: options.stripEnv as ConfigSection['stripEnv'], fromToml: options.fromToml, toToml: options.toToml, deprecations: options.deprecations, collectDiagnostics: options.collectDiagnostics, }); this._onDidRegisterSection.fire({ domain }); } unregisterSection(domain: string): void { if (!this.sections.delete(domain)) return; this._onDidUnregisterSection.fire({ domain }); } getSection(domain: string): ConfigSection | undefined { return this.sections.get(domain); } listSections(): readonly ConfigSection[] { return [...this.sections.values()]; } registerEffectiveOverlay(overlay: ConfigEffectiveOverlay): void { this.overlays.push(overlay); this._onDidRegisterOverlay.fire({ overlay }); } listEffectiveOverlays(): readonly ConfigEffectiveOverlay[] { return [...this.overlays]; } validate(domain: string, value: unknown): T { const schema = this.sections.get(domain)?.schema; return (schema === undefined ? value : schema.parse(value)) as T; } merge(domain: string, base: T | undefined, patch: unknown): T { const merge = this.sections.get(domain)?.merge ?? deepMerge; return merge(base, patch) as T; } defaultValue(domain: string): T | undefined { return this.sections.get(domain)?.defaultValue as T | undefined; } } export class ConfigService extends Disposable implements IConfigService { declare readonly _serviceBrand: undefined; private readonly _onDidChangeConfiguration = this._register(new Emitter()); readonly onDidChangeConfiguration: Event = this._onDidChangeConfiguration.event; private readonly _onDidSectionChange = this._register(new Emitter()); readonly onDidSectionChange: Event = this._onDidSectionChange.event; private readonly _onDidChangeDiagnostics = this._register( new Emitter(), ); readonly onDidChangeDiagnostics: Event = this._onDidChangeDiagnostics.event; readonly ready: Promise; private stateChain: Promise = Promise.resolve(); private readonly watchDebounce = this._register(new TimeoutTimer()); private rawSnake: ResolvedConfig = {}; private raw: ResolvedConfig = {}; private validated: ResolvedConfig = {}; private effective: ResolvedConfig = {}; private memory: ResolvedConfig = {}; private delivered: ResolvedConfig = {}; private readonly diagnosticsList: ConfigDiagnostic[] = []; private lastDiagnosticsSnapshot = '[]'; private readonly configKey: string; private tainted = false; constructor( @IConfigRegistry private readonly registry: IConfigRegistry, @IBootstrapService private readonly bootstrap: IBootstrapService, @ILogService private readonly log: ILogService, @IAtomicTomlDocumentStore private readonly documentStore: IAtomicTomlDocumentStore, ) { super(); this.configKey = this.bootstrap.configKey; this._register(this.registry.onDidRegisterSection((e) => this.revalidateDomain(e.domain))); this._register(this.registry.onDidUnregisterSection((e) => this.devalidateDomain(e.domain))); this._register(this.registry.onDidRegisterOverlay(() => this.reapplyOverlays())); const { configKey } = this; const { homeDir } = this.bootstrap; this.seedInitialLoad(); this.ready = this.load('load'); const configFile = join(homeDir, configKey); const handle = watch(homeDir, { depth: 0 }); this._register(handle); this._register( handle.onDidChange((change) => { if (normalize(change.path) !== normalize(configFile)) return; this.watchDebounce.cancelAndSet(() => { void this.reload(); }, WATCH_DEBOUNCE_MS); }), ); } get(domain: string): T { if (Object.prototype.hasOwnProperty.call(this.memory, domain)) { return this.memory[domain] as T; } return this.freshEffective()[domain] as T; } inspect(domain: string): ConfigInspectValue { const memoryValue = this.memory[domain] as T | undefined; return { value: this.get(domain), defaultValue: this.registry.defaultValue(domain), userValue: this.raw[domain] as T | undefined, memoryValue, }; } getAll(): ResolvedConfig { return { ...this.freshEffective(), ...this.memory }; } private freshEffective(): ResolvedConfig { const effective: ResolvedConfig = { ...this.validated }; this.applySectionEnvBindings(effective, false); this.applyEnvOverlay(effective, false); return effective; } diagnostics(): readonly ConfigDiagnostic[] { return [...this.diagnosticsList]; } private pushDiagnostic(diagnostic: ConfigDiagnostic): void { const duplicate = this.diagnosticsList.some( (existing) => existing.domain === diagnostic.domain && existing.severity === diagnostic.severity && existing.message === diagnostic.message, ); if (!duplicate) this.diagnosticsList.push(diagnostic); } private emitDiagnosticsIfChanged(): void { const snapshot = JSON.stringify(this.diagnosticsList); if (snapshot === this.lastDiagnosticsSnapshot) return; this.lastDiagnosticsSnapshot = snapshot; this._onDidChangeDiagnostics.fire(this.diagnostics()); } async set( domain: string, patch: unknown, target: ConfigTarget = ConfigTarget.User, ): Promise { await this.ready; if (target === ConfigTarget.Memory) { const next = this.registry.merge(domain, this.memory[domain], patch); const validated = this.registry.validate(domain, next); if (validated === undefined) { delete this.memory[domain]; } else { this.memory[domain] = validated; } this.commit('set', [domain]); return; } await this.enqueueStateTransition(async () => { this.assertPersistable(); await this.persist(domain, (stagedRaw, stagedRawSnake) => { const next = this.registry.merge(domain, stagedRaw[domain], patch); const validated = this.registry.validate(domain, next); const stripped = this.stripEnv(domain, validated, stagedRaw, stagedRawSnake); if (stripped === undefined) { delete stagedRaw[domain]; } else { this.registry.validate(domain, stripped); stagedRaw[domain] = stripped; } }); this.rebuildEffective('set', [domain]); }); } async replace( domain: string, value: unknown, target: ConfigTarget = ConfigTarget.User, ): Promise { await this.ready; const effectiveValue = value === null ? undefined : value; if (target === ConfigTarget.Memory) { if (effectiveValue === undefined) { delete this.memory[domain]; } else { this.memory[domain] = this.registry.validate(domain, effectiveValue); } this.commit('set', [domain]); return; } await this.enqueueStateTransition(async () => { this.assertPersistable(); await this.persist(domain, (stagedRaw, stagedRawSnake) => { const stripped = this.stripEnv(domain, effectiveValue, stagedRaw, stagedRawSnake); if (stripped === undefined) { delete stagedRaw[domain]; } else { stagedRaw[domain] = this.registry.validate(domain, stripped); } }); this.rebuildEffective('set', [domain]); }); } async replaceSections( sections: Readonly>, target: ConfigTarget = ConfigTarget.User, ): Promise { await this.ready; const domains = Object.keys(sections); if (domains.length === 0) return; if (target === ConfigTarget.Memory) { const staged: ResolvedConfig = { ...this.memory }; for (const domain of domains) { const value = sections[domain]; if (value === undefined || value === null) { delete staged[domain]; } else { staged[domain] = this.registry.validate(domain, value); } } this.memory = staged; this.commit('set', domains); return; } await this.enqueueStateTransition(async () => { this.assertPersistable(); await this.persistDomains(domains, (stagedRaw, stagedRawSnake) => { for (const domain of domains) { const value = sections[domain] === null ? undefined : sections[domain]; const stripped = this.stripEnv(domain, value, stagedRaw, stagedRawSnake); if (stripped === undefined) { delete stagedRaw[domain]; } else { stagedRaw[domain] = this.registry.validate(domain, stripped); } } }); this.rebuildEffective('set', domains); }); } private stripEnv( domain: string, value: unknown, raw: ResolvedConfig, rawSnake: ResolvedConfig, ): unknown { let result = value; const section = this.registry.getSection(domain); if (section?.stripEnv !== undefined) { const getEnv = (name: string): string | undefined => this.bootstrap.getEnv(name); result = section.stripEnv(result, raw[domain], getEnv); } if (result === undefined) return result; for (const overlay of this.registry.listEffectiveOverlays()) { if (overlay.strip === undefined) continue; result = overlay.strip(domain, result, rawSnake); if (result === undefined) return result; } return result; } async reload(): Promise { await this.ready; await this.enqueueStateTransition(() => this.load('reload')); } private enqueueStateTransition(fn: () => Promise): Promise { const run = this.stateChain.then(() => fn()); this.stateChain = run.then( () => undefined, () => undefined, ); return run; } private seedInitialLoad(): void { let fileData: ResolvedConfig; try { const text = readFileSync(this.bootstrap.configPath, 'utf8'); const data: unknown = text.trim().length === 0 ? {} : parseToml(text); if (!isPlainObject(data)) return; fileData = data; } catch { return; } this.rawSnake = cloneRecord(fileData); this.raw = transformTomlData(fileData, this.registry); this.validated = this.buildValidated(this.raw); const next = { ...this.validated }; this.applySectionEnvBindings(next, true); this.applyEnvOverlay(next); this.effective = next; } private async load(source: ConfigChangeSource): Promise { this.diagnosticsList.length = 0; let fileData: ResolvedConfig = {}; let failed = false; try { const data = await this.documentStore.get(CONFIG_SCOPE, this.configKey); fileData = data !== undefined && isPlainObject(data) ? data : {}; } catch (error) { failed = true; const message = error instanceof TomlError ? `Failed to parse ${this.bootstrap.configPath}: ${describeTomlSyntaxError(error)}` : describeUnknownError(error); this.pushDiagnostic({ severity: 'error', message }); this.log.warn('config load failed', { error: describeUnknownError(error) }); if (source !== 'load') { this.tainted = true; this.emitDiagnosticsIfChanged(); return; } } this.tainted = failed; const nextRawSnake = cloneRecord(fileData); for (const diagnostic of collectKeyDeprecations(nextRawSnake, this.registry.listSections())) { this.pushDiagnostic(diagnostic); } for (const section of this.registry.listSections()) { if (section.collectDiagnostics === undefined) continue; const rawSection = nextRawSnake[camelToSnake(section.domain)]; for (const diagnostic of section.collectDiagnostics(rawSection)) { this.pushDiagnostic(diagnostic); } } if (source !== 'load' && JSON.stringify(nextRawSnake) === JSON.stringify(this.rawSnake)) { const scratch = { ...this.validated }; this.applySectionEnvBindings(scratch, true); this.applyEnvOverlay(scratch); this.emitDiagnosticsIfChanged(); return; } this.rawSnake = nextRawSnake; this.raw = transformTomlData(fileData, this.registry); this.rebuildEffective(source); } private rebuildEffective( source: ConfigChangeSource = 'reload', domains?: readonly string[], ): void { const previous = this.effective; this.validated = this.buildValidated(this.raw); const next = { ...this.validated }; this.applySectionEnvBindings(next, true); this.applyEnvOverlay(next); this.effective = next; const candidates = new Set( domains ?? [...Object.keys(previous), ...Object.keys(next)], ); for (const domain of new Set([...Object.keys(previous), ...Object.keys(next)])) { if (!deepEqual(previous[domain], next[domain])) candidates.add(domain); } this.commit(source, [...candidates]); this.emitDiagnosticsIfChanged(); } private deliveredValue(domain: string): unknown { return Object.prototype.hasOwnProperty.call(this.memory, domain) ? this.memory[domain] : this.effective[domain]; } private commit(source: ConfigChangeSource, domains: readonly string[]): void { for (const domain of domains) { const previousValue = this.delivered[domain]; const value = this.deliveredValue(domain); this._onDidChangeConfiguration.fire({ domain, source, value, previousValue }); if (!deepEqual(value, previousValue)) { this._onDidSectionChange.fire({ domain, source, value, previousValue }); } this.delivered[domain] = value; } } private buildValidated(raw: ResolvedConfig): ResolvedConfig { const validated: ResolvedConfig = {}; for (const [domain, value] of Object.entries(raw)) { try { validated[domain] = this.registry.validate(domain, value); } catch (error) { this.pushDiagnostic({ domain, severity: 'warning', message: `Ignored invalid config section '${domain}': ${describeUnknownError(error)}`, }); } } for (const section of this.registry.listSections()) { if (validated[section.domain] === undefined && section.defaultValue !== undefined) { validated[section.domain] = section.defaultValue; } } return validated; } private applySectionEnvBindings(effective: ResolvedConfig, reportErrors: boolean): void { const getEnv = (name: string): string | undefined => this.bootstrap.getEnv(name); for (const section of this.registry.listSections()) { if (section.env === undefined) continue; try { const base = effective[section.domain]; const onDeprecatedEnv: OnDeprecatedEnv | undefined = reportErrors ? (oldName, newName) => { this.pushDiagnostic({ domain: section.domain, severity: 'warning', message: `Environment variable ${oldName} is deprecated; use ${newName} instead.`, }); } : undefined; const next = applySectionEnv(base, section.env, getEnv, onDeprecatedEnv); effective[section.domain] = this.registry.validate(section.domain, next); } catch (error) { if (reportErrors) { this.pushDiagnostic({ domain: section.domain, severity: 'warning', message: `Ignoring env overlay for '${section.domain}': ${describeUnknownError(error)}`, }); } } } } private applyEnvOverlay(effective: ResolvedConfig, reportErrors = true): void { const getEnv = (name: string): string | undefined => this.bootstrap.getEnv(name); const validate = (domain: string, value: unknown): unknown => this.registry.validate(domain, value); for (const overlay of this.registry.listEffectiveOverlays()) { try { overlay.apply(effective, getEnv, validate); } catch (error) { if (reportErrors) { this.pushDiagnostic({ severity: 'warning', message: `Ignoring config environment overlay: ${describeUnknownError(error)}`, }); } } } } private reapplyOverlays(): void { const before = this.effective; this.validated = this.buildValidated(this.raw); const next = { ...this.validated }; this.applySectionEnvBindings(next, true); this.applyEnvOverlay(next); this.effective = next; this.commit('reload', [...new Set([...Object.keys(before), ...Object.keys(next)])]); this.emitDiagnosticsIfChanged(); } private revalidateDomain(domain: string): void { const section = this.registry.getSection(domain); if (section === undefined) return; if (section.fromToml !== undefined) { const rawSnakeValue = this.rawSnake[camelToSnake(domain)]; if (rawSnakeValue !== undefined) { this.raw[domain] = section.fromToml(rawSnakeValue); } } if (this.raw[domain] !== undefined) { try { const validatedValue = this.registry.validate(domain, this.raw[domain]); this.validated[domain] = validatedValue; this.effective[domain] = validatedValue; } catch { return; } } else if (section.defaultValue !== undefined && this.effective[domain] === undefined) { this.validated[domain] = section.defaultValue; this.effective[domain] = section.defaultValue; } else { return; } this.applyEnvOverlay(this.effective); if (section.env !== undefined) { const getEnv = (name: string): string | undefined => this.bootstrap.getEnv(name); try { const onDeprecatedEnv: OnDeprecatedEnv = (oldName, newName) => { this.pushDiagnostic({ domain, severity: 'warning', message: `Environment variable ${oldName} is deprecated; use ${newName} instead.`, }); }; const next = applySectionEnv(this.effective[domain], section.env, getEnv, onDeprecatedEnv); this.effective[domain] = this.registry.validate(domain, next); } catch (error) { this.pushDiagnostic({ domain, severity: 'warning', message: `Ignoring env overlay for '${domain}': ${describeUnknownError(error)}`, }); } } this.commit('reload', [domain]); this.emitDiagnosticsIfChanged(); } private devalidateDomain(domain: string): void { if (this.registry.getSection(domain) !== undefined) return; const snakeKey = camelToSnake(domain); const rawSnakeValue = this.rawSnake[snakeKey]; if (rawSnakeValue === undefined) { delete this.raw[domain]; delete this.validated[domain]; delete this.effective[domain]; } else { const raw = transformTomlData({ [snakeKey]: rawSnakeValue }, this.registry)[domain]; this.raw[domain] = raw; this.validated[domain] = raw; this.effective[domain] = raw; } this.applyEnvOverlay(this.effective); this.commit('reload', [domain]); } private assertPersistable(): void { if (!this.tainted) return; throw new Error2( ErrorCodes.CONFIG_PERSIST_BLOCKED, `Refusing to persist config: ${this.bootstrap.configPath} could not be read; fix the file and reload before writing.`, ); } private async persist( domain: string, rebase: (stagedRaw: ResolvedConfig, stagedRawSnake: ResolvedConfig) => void, ): Promise { await this.persistDomains([domain], rebase); } private async persistDomains( domains: readonly string[], rebase: (stagedRaw: ResolvedConfig, stagedRawSnake: ResolvedConfig) => void, ): Promise { this.assertPersistable(); let onDisk: ResolvedConfig = {}; try { const data = await this.documentStore.get(CONFIG_SCOPE, this.configKey); onDisk = data !== undefined && isPlainObject(data) ? data : {}; } catch (error) { const message = error instanceof TomlError ? `Failed to parse ${this.bootstrap.configPath}: ${describeTomlSyntaxError(error)}` : describeUnknownError(error); this.pushDiagnostic({ severity: 'error', message }); this.emitDiagnosticsIfChanged(); this.log.warn('config persist aborted: re-read failed', { error: describeUnknownError(error), }); this.tainted = true; throw new Error2( ErrorCodes.CONFIG_PERSIST_BLOCKED, `Refusing to persist config: ${this.bootstrap.configPath} could not be read; fix the file and reload before writing.`, { cause: error }, ); } let onDiskText: string | undefined; try { onDiskText = await this.documentStore.getText(CONFIG_SCOPE, this.configKey); } catch { onDiskText = undefined; } const stagedRawSnake = cloneRecord(onDisk); const stagedRaw = transformTomlData(onDisk, this.registry); const previousSnake: ResolvedConfig = {}; for (const domain of domains) { const snakeKey = camelToSnake(domain); previousSnake[snakeKey] = stagedRawSnake[snakeKey]; } rebase(stagedRaw, stagedRawSnake); for (const domain of domains) { applySectionToToml(stagedRawSnake, domain, stagedRaw[domain], this.registry); } const plannedText = onDiskText === undefined ? undefined : planConfigWriteback( onDiskText, domains.map((domain) => { const snakeKey = camelToSnake(domain); return { snakeKey, previousValue: previousSnake[snakeKey], nextValue: stagedRawSnake[snakeKey], }; }), stagedRawSnake, ); if (plannedText === undefined) { await this.documentStore.set(CONFIG_SCOPE, this.configKey, stagedRawSnake); } else if (plannedText !== onDiskText) { await this.documentStore.setText(CONFIG_SCOPE, this.configKey, plannedText); } this.rawSnake = stagedRawSnake; this.raw = stagedRaw; } } registerScopedService( LifecycleScope.App, IConfigRegistry, ConfigRegistry, ScopeActivation.OnScopeCreated, 'config', ); registerScopedService( LifecycleScope.App, IConfigService, ConfigService, ScopeActivation.OnScopeCreated, 'config', );