/** * @license * Copyright 2022-2026 Matter.js Authors * SPDX-License-Identifier: Apache-2.0 */ import { ImplementationError } from "../MatterError.js"; import { Diagnostic } from "../log/Diagnostic.js"; import { isObject } from "../util/Type.js"; import type { Environment } from "./Environment.js"; import { Environmental } from "./Environmental.js"; /** * Environmental configuration values. */ export class VariableService { #vars = {} as VariableService.Map; #usageCollectors = Array>(); #usages = new Set(); constructor(environment: Environment) { environment.set(VariableService, this); } static [Environmental.create](environment: Environment) { return new this(environment); } get [Diagnostic.value]() { return "Configuration"; } get vars() { return this.#vars; } /** * Configure a component. * * Runs the supplied {@link configurator} immediately and whenever variables reference by the {@link configurator} * change. * * To terminate reconfiguration invoke {@link VariableService.Usage.close} on the return value. */ use(configurator: () => void): VariableService.Usage { const variables = new Set(); try { this.#usageCollectors.push(variables); configurator(); } finally { this.#usageCollectors.pop(); } const usage = { variables, configurator, close: () => { this.#usages.delete(usage); }, }; this.#usages.add(usage); return usage; } get(name: string, fallback?: T): T; get(name: string, fallback: T): T; get(name: string): T | undefined; get(name: string, fallback?: VariableService.Value) { for (const collector of this.#usageCollectors) { collector.add(name); } switch (typeof fallback) { case "string": return this.string(name) ?? fallback; case "number": return this.number(name) ?? fallback; case "boolean": return this.boolean(name) ?? fallback; } let value: VariableService.Value = this.#vars; for (const segment of this.#parseName(name)) { if (value === null || typeof value !== "object" || Array.isArray(value)) { return fallback; } value = value[segment]; } return value ?? fallback; } has(name: string) { return this.get(name) !== undefined; } set(name: string, value: VariableService.Value) { const segments = name.toLowerCase().split("."); const key = segments.pop() as string; if (key === "__proto__" || key === "constructor" || key === "prototype") { throw new ImplementationError(`Invalid variable path name: ${key}`); } let parent: VariableService.Map = this.#vars; for (const segment of segments) { let nextParent = parent[segment]; if (nextParent === undefined || nextParent === null) { nextParent = parent[segment] = {}; } else if (typeof nextParent !== "object" || Array.isArray(nextParent)) { throw new ImplementationError(`Can't set ${name} because segment ${segment} is not a map`); } parent = nextParent; } if (parent[key] === value) { return; } parent[key] = value; for (const usage of this.#usages) { if (usage.variables.has(name)) { usage.configurator(); } } } async persist(name: string, value: VariableService.Value) { this.set(name, value); await this.persistConfigValue?.(name, value); } persistConfigValue?: (name: string, value: VariableService.Value) => Promise; string(name: string) { const value = this.get(name); if (value === undefined) { return value; } if (typeof value === "string") { return value; } if (value === null) { return "null"; } return value.toString(); } boolean(name: string) { const value = this.get(name); switch (value) { case undefined: return value; case null: case 0: case "0": case false: case "false": case "off": return false; default: return true; } } number(name: string) { let value = this.get(name); if (typeof value === "number") { return value; } if (typeof value === "string") { value = Number.parseFloat(value); if (!Number.isFinite(value)) { return; } return value; } } integer(name: string, fallback?: number) { const number = this.number(name) ?? fallback; if (typeof number === "number") { return Math.floor(number); } } increment(name: string) { const value = this.integer(name) ?? 0; this.set(name, value + 1); return value; } addConfigStyle(vars: VariableService.Map) { const path = Array(); const add = (vars: VariableService.Map) => { for (const name in vars) { const val = vars[name]; if (isObject(val)) { path.push(name); add(val); path.pop(); } else { this.set([...path, name].join("."), val); } } }; add(vars); } addUnixEnvStyle(vars: Record) { this.addConfigStyle(parseUnixStyle(vars)); } addArgvStyle(vars: string[]) { this.addConfigStyle(parseArgvStyle(vars)); } #parseName(name: string) { return name .toLowerCase() .split(".") .map(segment => { segment = segment.trim(); if (segment === "") { throw new ImplementationError(`Variable name ${name} contains empty segments`); } return segment; }); } } export namespace VariableService { export interface Options { unixEnvStyle?: Record; argvStyle?: string[]; configStyle?: Map; } export interface Map extends Record {} export type List = Value[]; export type Primitive = number | string | boolean; export type Value = Primitive | Map | List; export interface Usage { variables: Set; configurator(): void; close(): void; } } function addVariable(into: Record, path: string[], value: any) { if (!path.length) { return; } const firstPathEntry = path[0]; if (firstPathEntry === "__proto__" || firstPathEntry === "constructor" || firstPathEntry === "prototype") { throw new ImplementationError(`Invalid variable path name: ${firstPathEntry}`); } let current = into[firstPathEntry]; if (path.length === 1) { if (current === undefined) { into[firstPathEntry] = value; } return; } if (typeof current !== "object") { current = into[firstPathEntry] = {}; } addVariable(current, path.slice(1), value); } function parseUnixStyle(values: Record) { const variables = {} as VariableService.Map; for (const key in values) { if (key.startsWith("MATTER_")) { if (values[key] === undefined || values[key] === "") { continue; } addVariable(variables, key.slice(7).toLowerCase().split("_"), values[key]); } } return variables; } function parseArgvStyle(values: string[]) { const variables = {} as VariableService.Map; for (let arg of values) { if (!arg.startsWith("--")) { continue; } arg = arg.slice(2); const separatorPos = arg.indexOf("="); let key, value; if (separatorPos === -1) { key = arg; value = true; } else { key = arg.slice(0, separatorPos); value = arg.slice(separatorPos + 1); if ((value.startsWith("'") && value.endsWith("'")) || (value.startsWith('"') && value.endsWith('"'))) { value = value.slice(1, -1).replaceAll("\\'", "'").replaceAll('\\"', '"'); } } addVariable(variables, key.toLowerCase().split("-"), value); } return variables; }