@smplkit/sdk 1.1.4 → 1.1.6

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.
@@ -20,7 +20,7 @@ function resolveChain(chain, environment) {
20
20
  let accumulated = {};
21
21
  for (let i = chain.length - 1; i >= 0; i--) {
22
22
  const config = chain[i];
23
- const baseValues = config.values ?? {};
23
+ const baseValues = config.items ?? {};
24
24
  const envEntry = (config.environments ?? {})[environment];
25
25
  const envValues = envEntry !== null && envEntry !== void 0 && typeof envEntry === "object" && !Array.isArray(envEntry) ? envEntry.values ?? {} : {};
26
26
  const configResolved = deepMerge(baseValues, envValues);
@@ -274,14 +274,14 @@ var ConfigRuntime = class {
274
274
  const envEntry = chainEntry.environments[this._environment] !== void 0 && chainEntry.environments[this._environment] !== null ? chainEntry.environments[this._environment] : null;
275
275
  const envValues = envEntry !== null && typeof envEntry === "object" ? envEntry.values ?? {} : null;
276
276
  if (new_value === null || new_value === void 0) {
277
- delete chainEntry.values[key];
277
+ delete chainEntry.items[key];
278
278
  if (envValues) delete envValues[key];
279
279
  } else if (envValues && key in envValues) {
280
280
  envValues[key] = new_value;
281
- } else if (key in chainEntry.values) {
282
- chainEntry.values[key] = new_value;
281
+ } else if (key in chainEntry.items) {
282
+ chainEntry.items[key] = new_value;
283
283
  } else {
284
- chainEntry.values[key] = new_value;
284
+ chainEntry.items[key] = new_value;
285
285
  }
286
286
  }
287
287
  const oldCache = this._cache;
@@ -314,4 +314,4 @@ var ConfigRuntime = class {
314
314
  export {
315
315
  ConfigRuntime
316
316
  };
317
- //# sourceMappingURL=chunk-GLOLTIGH.js.map
317
+ //# sourceMappingURL=chunk-RF6LYU4V.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/config/runtime.ts","../src/config/resolve.ts"],"sourcesContent":["/**\n * ConfigRuntime — runtime-plane value resolution with WebSocket updates.\n *\n * Holds a fully resolved local cache of config values for a specific\n * environment. All value-access methods are synchronous (local reads);\n * only {@link refresh} and {@link close} are async.\n *\n * A background WebSocket connection is maintained for real-time updates.\n * If the WebSocket fails, the runtime operates in cache-only mode and\n * reconnects automatically with exponential backoff.\n */\n\nimport WebSocket from \"ws\";\nimport { resolveChain } from \"./resolve.js\";\nimport type { ChainConfig } from \"./resolve.js\";\nimport type { ConfigChangeEvent, ConfigStats, ConnectionStatus } from \"./runtime-types.js\";\n\n/** @internal */\ninterface ChangeListener {\n callback: (event: ConfigChangeEvent) => void;\n key: string | null;\n}\n\n/** @internal */\ninterface WsConfigChangedMessage {\n type: \"config_changed\";\n config_id: string;\n changes: Array<{\n key: string;\n old_value: unknown;\n new_value: unknown;\n }>;\n}\n\n/** @internal */\ninterface WsConfigDeletedMessage {\n type: \"config_deleted\";\n config_id: string;\n}\n\ntype WsMessage =\n | { type: \"subscribed\"; config_id: string; environment: string }\n | { type: \"error\"; message: string }\n | WsConfigChangedMessage\n | WsConfigDeletedMessage;\n\n/** @internal */\nconst BACKOFF_MS = [1000, 2000, 4000, 8000, 16000, 32000, 60000];\n\n/** @internal Options for constructing a ConfigRuntime. */\nexport interface ConfigRuntimeOptions {\n configKey: string;\n configId: string;\n environment: string;\n chain: ChainConfig[];\n apiKey: string;\n baseUrl: string;\n fetchChain: (() => Promise<ChainConfig[]>) | null;\n}\n\n/**\n * Runtime configuration handle for a specific environment.\n *\n * Obtained by calling {@link Config.connect}. All value-access methods\n * are synchronous and served entirely from a local in-process cache.\n * The cache is populated eagerly on construction and kept current via\n * a background WebSocket connection.\n */\nexport class ConfigRuntime {\n private _cache: Record<string, unknown>;\n private _chain: ChainConfig[];\n private _fetchCount: number;\n private _lastFetchAt: string | null;\n private _closed = false;\n private _wsStatus: ConnectionStatus = \"disconnected\";\n private _ws: InstanceType<typeof WebSocket> | null = null;\n private _reconnectTimer: ReturnType<typeof setTimeout> | null = null;\n private _backoffIndex = 0;\n private _listeners: ChangeListener[] = [];\n\n private readonly _configId: string;\n private readonly _environment: string;\n private readonly _apiKey: string;\n private readonly _baseUrl: string;\n private readonly _fetchChain: (() => Promise<ChainConfig[]>) | null;\n\n /** @internal */\n constructor(options: ConfigRuntimeOptions) {\n this._configId = options.configId;\n this._environment = options.environment;\n this._apiKey = options.apiKey;\n this._baseUrl = options.baseUrl;\n this._fetchChain = options.fetchChain;\n this._chain = options.chain;\n this._cache = resolveChain(options.chain, options.environment);\n this._fetchCount = options.chain.length;\n this._lastFetchAt = new Date().toISOString();\n\n // Start WebSocket in background — non-blocking\n this._connectWebSocket();\n }\n\n // ---- Value access (synchronous, local cache) ----\n\n /**\n * Return the resolved value for `key`, or `defaultValue` if absent.\n *\n * @param key - The config key to look up.\n * @param defaultValue - Returned when the key is not present (default: null).\n */\n get(key: string, defaultValue: unknown = null): unknown {\n return key in this._cache ? this._cache[key] : defaultValue;\n }\n\n /**\n * Return the value as a string, or `defaultValue` if absent or not a string.\n */\n getString(key: string, defaultValue: string | null = null): string | null {\n const value = this._cache[key];\n return typeof value === \"string\" ? value : defaultValue;\n }\n\n /**\n * Return the value as a number, or `defaultValue` if absent or not a number.\n */\n getInt(key: string, defaultValue: number | null = null): number | null {\n const value = this._cache[key];\n return typeof value === \"number\" ? value : defaultValue;\n }\n\n /**\n * Return the value as a boolean, or `defaultValue` if absent or not a boolean.\n */\n getBool(key: string, defaultValue: boolean | null = null): boolean | null {\n const value = this._cache[key];\n return typeof value === \"boolean\" ? value : defaultValue;\n }\n\n /**\n * Return whether `key` is present in the resolved configuration.\n */\n exists(key: string): boolean {\n return key in this._cache;\n }\n\n /**\n * Return a shallow copy of the full resolved configuration.\n */\n getAll(): Record<string, unknown> {\n return { ...this._cache };\n }\n\n // ---- Change listeners ----\n\n /**\n * Register a listener that fires when a config value changes.\n *\n * @param callback - Called with a {@link ConfigChangeEvent} on each change.\n * @param options.key - If provided, the listener fires only for this key.\n * If omitted, the listener fires for all changes.\n */\n onChange(callback: (event: ConfigChangeEvent) => void, options?: { key?: string }): void {\n this._listeners.push({\n callback,\n key: options?.key ?? null,\n });\n }\n\n // ---- Diagnostics ----\n\n /**\n * Return diagnostic statistics for this runtime.\n */\n stats(): ConfigStats {\n return {\n fetchCount: this._fetchCount,\n lastFetchAt: this._lastFetchAt,\n };\n }\n\n /**\n * Return the current WebSocket connection status.\n */\n connectionStatus(): ConnectionStatus {\n return this._wsStatus;\n }\n\n // ---- Lifecycle ----\n\n /**\n * Force a manual refresh of the cached configuration.\n *\n * Re-fetches the full config chain via HTTP, re-resolves values, updates\n * the local cache, and fires listeners for any detected changes.\n *\n * @throws {Error} If no `fetchChain` function was provided on construction.\n */\n async refresh(): Promise<void> {\n if (!this._fetchChain) {\n throw new Error(\"No fetchChain function provided; cannot refresh.\");\n }\n\n const newChain = await this._fetchChain();\n const oldCache = this._cache;\n\n this._chain = newChain;\n this._cache = resolveChain(newChain, this._environment);\n this._fetchCount += newChain.length;\n this._lastFetchAt = new Date().toISOString();\n\n this._diffAndFire(oldCache, this._cache, \"manual\");\n }\n\n /**\n * Close the runtime connection.\n *\n * Shuts down the WebSocket and cancels any pending reconnect timer.\n * Safe to call multiple times.\n */\n async close(): Promise<void> {\n this._closed = true;\n this._wsStatus = \"disconnected\";\n\n if (this._reconnectTimer !== null) {\n clearTimeout(this._reconnectTimer);\n this._reconnectTimer = null;\n }\n\n if (this._ws !== null) {\n this._ws.close();\n this._ws = null;\n }\n }\n\n /**\n * Async dispose support for `await using` (TypeScript 5.2+).\n */\n async [Symbol.asyncDispose](): Promise<void> {\n await this.close();\n }\n\n // ---- WebSocket internals ----\n\n private _buildWsUrl(): string {\n let url = this._baseUrl;\n if (url.startsWith(\"https://\")) {\n url = \"wss://\" + url.slice(\"https://\".length);\n } else if (url.startsWith(\"http://\")) {\n url = \"ws://\" + url.slice(\"http://\".length);\n } else {\n url = \"wss://\" + url;\n }\n url = url.replace(/\\/$/, \"\");\n return `${url}/api/ws/v1/configs?api_key=${this._apiKey}`;\n }\n\n private _connectWebSocket(): void {\n if (this._closed) return;\n\n this._wsStatus = \"connecting\";\n const wsUrl = this._buildWsUrl();\n\n try {\n const ws = new WebSocket(wsUrl);\n this._ws = ws;\n\n ws.on(\"open\", () => {\n if (this._closed) {\n ws.close();\n return;\n }\n this._backoffIndex = 0;\n this._wsStatus = \"connected\";\n ws.send(\n JSON.stringify({\n type: \"subscribe\",\n config_id: this._configId,\n environment: this._environment,\n }),\n );\n });\n\n ws.on(\"message\", (data: WebSocket.RawData) => {\n try {\n const msg = JSON.parse(String(data)) as WsMessage;\n this._handleMessage(msg);\n } catch {\n // ignore unparseable messages\n }\n });\n\n ws.on(\"close\", () => {\n if (!this._closed) {\n this._wsStatus = \"disconnected\";\n this._scheduleReconnect();\n }\n });\n\n ws.on(\"error\", () => {\n // 'close' will fire after 'error'; reconnect is handled there\n });\n } catch {\n if (!this._closed) {\n this._scheduleReconnect();\n }\n }\n }\n\n private _scheduleReconnect(): void {\n if (this._closed) return;\n\n const delay = BACKOFF_MS[Math.min(this._backoffIndex, BACKOFF_MS.length - 1)];\n this._backoffIndex++;\n this._wsStatus = \"connecting\";\n\n this._reconnectTimer = setTimeout(() => {\n this._reconnectTimer = null;\n // On reconnect, resync the cache to pick up changes missed while offline\n if (this._fetchChain) {\n this._fetchChain()\n .then((newChain) => {\n const oldCache = this._cache;\n this._chain = newChain;\n this._cache = resolveChain(newChain, this._environment);\n this._fetchCount += newChain.length;\n this._lastFetchAt = new Date().toISOString();\n this._diffAndFire(oldCache, this._cache, \"manual\");\n })\n .catch(() => {\n // ignore fetch errors during reconnect\n })\n .finally(() => {\n this._connectWebSocket();\n });\n } else {\n this._connectWebSocket();\n }\n }, delay);\n }\n\n private _handleMessage(msg: WsMessage): void {\n if (msg.type === \"config_changed\") {\n this._applyChanges(msg.config_id, msg.changes);\n } else if (msg.type === \"config_deleted\") {\n this._closed = true;\n void this.close();\n }\n }\n\n private _applyChanges(\n configId: string,\n changes: Array<{ key: string; old_value: unknown; new_value: unknown }>,\n ): void {\n const chainEntry = this._chain.find((c) => c.id === configId);\n if (!chainEntry) return;\n\n for (const change of changes) {\n const { key, new_value } = change;\n\n // Get or create the environment entry\n const envEntry =\n chainEntry.environments[this._environment] !== undefined &&\n chainEntry.environments[this._environment] !== null\n ? (chainEntry.environments[this._environment] as Record<string, unknown>)\n : null;\n const envValues =\n envEntry !== null && typeof envEntry === \"object\"\n ? ((envEntry.values ?? {}) as Record<string, unknown>)\n : null;\n\n if (new_value === null || new_value === undefined) {\n // Deletion: remove from base values and env values\n delete chainEntry.values[key];\n if (envValues) delete envValues[key];\n } else if (envValues && key in envValues) {\n // Update existing env-specific override\n envValues[key] = new_value;\n } else if (key in chainEntry.values) {\n // Update existing base value\n chainEntry.values[key] = new_value;\n } else {\n // New key — put in base values\n chainEntry.values[key] = new_value;\n }\n }\n\n const oldCache = this._cache;\n this._cache = resolveChain(this._chain, this._environment);\n this._diffAndFire(oldCache, this._cache, \"websocket\");\n }\n\n private _diffAndFire(\n oldCache: Record<string, unknown>,\n newCache: Record<string, unknown>,\n source: \"websocket\" | \"poll\" | \"manual\",\n ): void {\n const allKeys = new Set([...Object.keys(oldCache), ...Object.keys(newCache)]);\n\n for (const key of allKeys) {\n const oldVal = key in oldCache ? oldCache[key] : null;\n const newVal = key in newCache ? newCache[key] : null;\n\n if (JSON.stringify(oldVal) !== JSON.stringify(newVal)) {\n const event: ConfigChangeEvent = { key, oldValue: oldVal, newValue: newVal, source };\n this._fireListeners(event);\n }\n }\n }\n\n private _fireListeners(event: ConfigChangeEvent): void {\n for (const listener of this._listeners) {\n if (listener.key === null || listener.key === event.key) {\n try {\n listener.callback(event);\n } catch {\n // ignore listener errors to prevent one bad listener from stopping others\n }\n }\n }\n }\n}\n","/**\n * Deep-merge resolution algorithm for config inheritance chains.\n *\n * Mirrors the Python SDK's `_resolver.py` (ADR-024 §2.5–2.6).\n */\n\n/** A single entry in a config inheritance chain (child-to-root ordering). */\nexport interface ChainConfig {\n /** Config UUID. */\n id: string;\n /** Base key-value pairs. */\n values: Record<string, unknown>;\n /**\n * Per-environment overrides.\n * Each entry is `{ values: { key: value, ... } }` — the server wraps\n * environment-specific values in a nested `values` key.\n */\n environments: Record<string, unknown>;\n}\n\n/**\n * Recursively merge two dicts, with `override` taking precedence.\n *\n * Nested dicts are merged recursively. Non-dict values (strings, numbers,\n * booleans, arrays, null) are replaced wholesale.\n */\nexport function deepMerge(\n base: Record<string, unknown>,\n override: Record<string, unknown>,\n): Record<string, unknown> {\n const result: Record<string, unknown> = { ...base };\n for (const [key, value] of Object.entries(override)) {\n if (\n key in result &&\n typeof result[key] === \"object\" &&\n result[key] !== null &&\n !Array.isArray(result[key]) &&\n typeof value === \"object\" &&\n value !== null &&\n !Array.isArray(value)\n ) {\n result[key] = deepMerge(\n result[key] as Record<string, unknown>,\n value as Record<string, unknown>,\n );\n } else {\n result[key] = value;\n }\n }\n return result;\n}\n\n/**\n * Resolve the full configuration for an environment given a config chain.\n *\n * Walks from root (last element) to child (first element), accumulating\n * values via deep merge so that child configs override parent configs.\n *\n * For each config in the chain, base `values` are merged with\n * environment-specific values (env wins), then that result is merged\n * on top of the accumulated parent result (child wins over parent).\n *\n * @param chain - Ordered list of config data from child (index 0) to root ancestor (last).\n * @param environment - The environment key to resolve for.\n */\nexport function resolveChain(chain: ChainConfig[], environment: string): Record<string, unknown> {\n let accumulated: Record<string, unknown> = {};\n\n // Walk from root to child (reverse order — chain is child-to-root)\n for (let i = chain.length - 1; i >= 0; i--) {\n const config = chain[i];\n const baseValues: Record<string, unknown> = config.values ?? {};\n\n // Environments are stored as { env_name: { values: { key: val } } }\n const envEntry = (config.environments ?? {})[environment];\n const envValues: Record<string, unknown> =\n envEntry !== null &&\n envEntry !== undefined &&\n typeof envEntry === \"object\" &&\n !Array.isArray(envEntry)\n ? (((envEntry as Record<string, unknown>).values ?? {}) as Record<string, unknown>)\n : {};\n\n // Merge environment overrides on top of base values (env wins)\n const configResolved = deepMerge(baseValues, envValues);\n\n // Merge this config's resolved values on top of accumulated parent values (child wins)\n accumulated = deepMerge(accumulated, configResolved);\n }\n\n return accumulated;\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/config/runtime.ts","../src/config/resolve.ts"],"sourcesContent":["/**\n * ConfigRuntime — runtime-plane value resolution with WebSocket updates.\n *\n * Holds a fully resolved local cache of config values for a specific\n * environment. All value-access methods are synchronous (local reads);\n * only {@link refresh} and {@link close} are async.\n *\n * A background WebSocket connection is maintained for real-time updates.\n * If the WebSocket fails, the runtime operates in cache-only mode and\n * reconnects automatically with exponential backoff.\n */\n\nimport WebSocket from \"ws\";\nimport { resolveChain } from \"./resolve.js\";\nimport type { ChainConfig } from \"./resolve.js\";\nimport type { ConfigChangeEvent, ConfigStats, ConnectionStatus } from \"./runtime-types.js\";\n\n/** @internal */\ninterface ChangeListener {\n callback: (event: ConfigChangeEvent) => void;\n key: string | null;\n}\n\n/** @internal */\ninterface WsConfigChangedMessage {\n type: \"config_changed\";\n config_id: string;\n changes: Array<{\n key: string;\n old_value: unknown;\n new_value: unknown;\n }>;\n}\n\n/** @internal */\ninterface WsConfigDeletedMessage {\n type: \"config_deleted\";\n config_id: string;\n}\n\ntype WsMessage =\n | { type: \"subscribed\"; config_id: string; environment: string }\n | { type: \"error\"; message: string }\n | WsConfigChangedMessage\n | WsConfigDeletedMessage;\n\n/** @internal */\nconst BACKOFF_MS = [1000, 2000, 4000, 8000, 16000, 32000, 60000];\n\n/** @internal Options for constructing a ConfigRuntime. */\nexport interface ConfigRuntimeOptions {\n configKey: string;\n configId: string;\n environment: string;\n chain: ChainConfig[];\n apiKey: string;\n baseUrl: string;\n fetchChain: (() => Promise<ChainConfig[]>) | null;\n}\n\n/**\n * Runtime configuration handle for a specific environment.\n *\n * Obtained by calling {@link Config.connect}. All value-access methods\n * are synchronous and served entirely from a local in-process cache.\n * The cache is populated eagerly on construction and kept current via\n * a background WebSocket connection.\n */\nexport class ConfigRuntime {\n private _cache: Record<string, unknown>;\n private _chain: ChainConfig[];\n private _fetchCount: number;\n private _lastFetchAt: string | null;\n private _closed = false;\n private _wsStatus: ConnectionStatus = \"disconnected\";\n private _ws: InstanceType<typeof WebSocket> | null = null;\n private _reconnectTimer: ReturnType<typeof setTimeout> | null = null;\n private _backoffIndex = 0;\n private _listeners: ChangeListener[] = [];\n\n private readonly _configId: string;\n private readonly _environment: string;\n private readonly _apiKey: string;\n private readonly _baseUrl: string;\n private readonly _fetchChain: (() => Promise<ChainConfig[]>) | null;\n\n /** @internal */\n constructor(options: ConfigRuntimeOptions) {\n this._configId = options.configId;\n this._environment = options.environment;\n this._apiKey = options.apiKey;\n this._baseUrl = options.baseUrl;\n this._fetchChain = options.fetchChain;\n this._chain = options.chain;\n this._cache = resolveChain(options.chain, options.environment);\n this._fetchCount = options.chain.length;\n this._lastFetchAt = new Date().toISOString();\n\n // Start WebSocket in background — non-blocking\n this._connectWebSocket();\n }\n\n // ---- Value access (synchronous, local cache) ----\n\n /**\n * Return the resolved value for `key`, or `defaultValue` if absent.\n *\n * @param key - The config key to look up.\n * @param defaultValue - Returned when the key is not present (default: null).\n */\n get(key: string, defaultValue: unknown = null): unknown {\n return key in this._cache ? this._cache[key] : defaultValue;\n }\n\n /**\n * Return the value as a string, or `defaultValue` if absent or not a string.\n */\n getString(key: string, defaultValue: string | null = null): string | null {\n const value = this._cache[key];\n return typeof value === \"string\" ? value : defaultValue;\n }\n\n /**\n * Return the value as a number, or `defaultValue` if absent or not a number.\n */\n getInt(key: string, defaultValue: number | null = null): number | null {\n const value = this._cache[key];\n return typeof value === \"number\" ? value : defaultValue;\n }\n\n /**\n * Return the value as a boolean, or `defaultValue` if absent or not a boolean.\n */\n getBool(key: string, defaultValue: boolean | null = null): boolean | null {\n const value = this._cache[key];\n return typeof value === \"boolean\" ? value : defaultValue;\n }\n\n /**\n * Return whether `key` is present in the resolved configuration.\n */\n exists(key: string): boolean {\n return key in this._cache;\n }\n\n /**\n * Return a shallow copy of the full resolved configuration.\n */\n getAll(): Record<string, unknown> {\n return { ...this._cache };\n }\n\n // ---- Change listeners ----\n\n /**\n * Register a listener that fires when a config value changes.\n *\n * @param callback - Called with a {@link ConfigChangeEvent} on each change.\n * @param options.key - If provided, the listener fires only for this key.\n * If omitted, the listener fires for all changes.\n */\n onChange(callback: (event: ConfigChangeEvent) => void, options?: { key?: string }): void {\n this._listeners.push({\n callback,\n key: options?.key ?? null,\n });\n }\n\n // ---- Diagnostics ----\n\n /**\n * Return diagnostic statistics for this runtime.\n */\n stats(): ConfigStats {\n return {\n fetchCount: this._fetchCount,\n lastFetchAt: this._lastFetchAt,\n };\n }\n\n /**\n * Return the current WebSocket connection status.\n */\n connectionStatus(): ConnectionStatus {\n return this._wsStatus;\n }\n\n // ---- Lifecycle ----\n\n /**\n * Force a manual refresh of the cached configuration.\n *\n * Re-fetches the full config chain via HTTP, re-resolves values, updates\n * the local cache, and fires listeners for any detected changes.\n *\n * @throws {Error} If no `fetchChain` function was provided on construction.\n */\n async refresh(): Promise<void> {\n if (!this._fetchChain) {\n throw new Error(\"No fetchChain function provided; cannot refresh.\");\n }\n\n const newChain = await this._fetchChain();\n const oldCache = this._cache;\n\n this._chain = newChain;\n this._cache = resolveChain(newChain, this._environment);\n this._fetchCount += newChain.length;\n this._lastFetchAt = new Date().toISOString();\n\n this._diffAndFire(oldCache, this._cache, \"manual\");\n }\n\n /**\n * Close the runtime connection.\n *\n * Shuts down the WebSocket and cancels any pending reconnect timer.\n * Safe to call multiple times.\n */\n async close(): Promise<void> {\n this._closed = true;\n this._wsStatus = \"disconnected\";\n\n if (this._reconnectTimer !== null) {\n clearTimeout(this._reconnectTimer);\n this._reconnectTimer = null;\n }\n\n if (this._ws !== null) {\n this._ws.close();\n this._ws = null;\n }\n }\n\n /**\n * Async dispose support for `await using` (TypeScript 5.2+).\n */\n async [Symbol.asyncDispose](): Promise<void> {\n await this.close();\n }\n\n // ---- WebSocket internals ----\n\n private _buildWsUrl(): string {\n let url = this._baseUrl;\n if (url.startsWith(\"https://\")) {\n url = \"wss://\" + url.slice(\"https://\".length);\n } else if (url.startsWith(\"http://\")) {\n url = \"ws://\" + url.slice(\"http://\".length);\n } else {\n url = \"wss://\" + url;\n }\n url = url.replace(/\\/$/, \"\");\n return `${url}/api/ws/v1/configs?api_key=${this._apiKey}`;\n }\n\n private _connectWebSocket(): void {\n if (this._closed) return;\n\n this._wsStatus = \"connecting\";\n const wsUrl = this._buildWsUrl();\n\n try {\n const ws = new WebSocket(wsUrl);\n this._ws = ws;\n\n ws.on(\"open\", () => {\n if (this._closed) {\n ws.close();\n return;\n }\n this._backoffIndex = 0;\n this._wsStatus = \"connected\";\n ws.send(\n JSON.stringify({\n type: \"subscribe\",\n config_id: this._configId,\n environment: this._environment,\n }),\n );\n });\n\n ws.on(\"message\", (data: WebSocket.RawData) => {\n try {\n const msg = JSON.parse(String(data)) as WsMessage;\n this._handleMessage(msg);\n } catch {\n // ignore unparseable messages\n }\n });\n\n ws.on(\"close\", () => {\n if (!this._closed) {\n this._wsStatus = \"disconnected\";\n this._scheduleReconnect();\n }\n });\n\n ws.on(\"error\", () => {\n // 'close' will fire after 'error'; reconnect is handled there\n });\n } catch {\n if (!this._closed) {\n this._scheduleReconnect();\n }\n }\n }\n\n private _scheduleReconnect(): void {\n if (this._closed) return;\n\n const delay = BACKOFF_MS[Math.min(this._backoffIndex, BACKOFF_MS.length - 1)];\n this._backoffIndex++;\n this._wsStatus = \"connecting\";\n\n this._reconnectTimer = setTimeout(() => {\n this._reconnectTimer = null;\n // On reconnect, resync the cache to pick up changes missed while offline\n if (this._fetchChain) {\n this._fetchChain()\n .then((newChain) => {\n const oldCache = this._cache;\n this._chain = newChain;\n this._cache = resolveChain(newChain, this._environment);\n this._fetchCount += newChain.length;\n this._lastFetchAt = new Date().toISOString();\n this._diffAndFire(oldCache, this._cache, \"manual\");\n })\n .catch(() => {\n // ignore fetch errors during reconnect\n })\n .finally(() => {\n this._connectWebSocket();\n });\n } else {\n this._connectWebSocket();\n }\n }, delay);\n }\n\n private _handleMessage(msg: WsMessage): void {\n if (msg.type === \"config_changed\") {\n this._applyChanges(msg.config_id, msg.changes);\n } else if (msg.type === \"config_deleted\") {\n this._closed = true;\n void this.close();\n }\n }\n\n private _applyChanges(\n configId: string,\n changes: Array<{ key: string; old_value: unknown; new_value: unknown }>,\n ): void {\n const chainEntry = this._chain.find((c) => c.id === configId);\n if (!chainEntry) return;\n\n for (const change of changes) {\n const { key, new_value } = change;\n\n // Get or create the environment entry\n const envEntry =\n chainEntry.environments[this._environment] !== undefined &&\n chainEntry.environments[this._environment] !== null\n ? (chainEntry.environments[this._environment] as Record<string, unknown>)\n : null;\n const envValues =\n envEntry !== null && typeof envEntry === \"object\"\n ? ((envEntry.values ?? {}) as Record<string, unknown>)\n : null;\n\n if (new_value === null || new_value === undefined) {\n // Deletion: remove from base items and env values\n delete chainEntry.items[key];\n if (envValues) delete envValues[key];\n } else if (envValues && key in envValues) {\n // Update existing env-specific override\n envValues[key] = new_value;\n } else if (key in chainEntry.items) {\n // Update existing base value\n chainEntry.items[key] = new_value;\n } else {\n // New key — put in base items\n chainEntry.items[key] = new_value;\n }\n }\n\n const oldCache = this._cache;\n this._cache = resolveChain(this._chain, this._environment);\n this._diffAndFire(oldCache, this._cache, \"websocket\");\n }\n\n private _diffAndFire(\n oldCache: Record<string, unknown>,\n newCache: Record<string, unknown>,\n source: \"websocket\" | \"poll\" | \"manual\",\n ): void {\n const allKeys = new Set([...Object.keys(oldCache), ...Object.keys(newCache)]);\n\n for (const key of allKeys) {\n const oldVal = key in oldCache ? oldCache[key] : null;\n const newVal = key in newCache ? newCache[key] : null;\n\n if (JSON.stringify(oldVal) !== JSON.stringify(newVal)) {\n const event: ConfigChangeEvent = { key, oldValue: oldVal, newValue: newVal, source };\n this._fireListeners(event);\n }\n }\n }\n\n private _fireListeners(event: ConfigChangeEvent): void {\n for (const listener of this._listeners) {\n if (listener.key === null || listener.key === event.key) {\n try {\n listener.callback(event);\n } catch {\n // ignore listener errors to prevent one bad listener from stopping others\n }\n }\n }\n }\n}\n","/**\n * Deep-merge resolution algorithm for config inheritance chains.\n *\n * Mirrors the Python SDK's `_resolver.py` (ADR-024 §2.5–2.6).\n */\n\n/** A single entry in a config inheritance chain (child-to-root ordering). */\nexport interface ChainConfig {\n /** Config UUID. */\n id: string;\n /** Base key-value pairs (unwrapped from typed item definitions). */\n items: Record<string, unknown>;\n /**\n * Per-environment overrides.\n * Each entry is `{ values: { key: value, ... } }` — values are already\n * unwrapped from the server's `{ value: raw }` wrapper by the client layer.\n */\n environments: Record<string, unknown>;\n}\n\n/**\n * Recursively merge two dicts, with `override` taking precedence.\n *\n * Nested dicts are merged recursively. Non-dict values (strings, numbers,\n * booleans, arrays, null) are replaced wholesale.\n */\nexport function deepMerge(\n base: Record<string, unknown>,\n override: Record<string, unknown>,\n): Record<string, unknown> {\n const result: Record<string, unknown> = { ...base };\n for (const [key, value] of Object.entries(override)) {\n if (\n key in result &&\n typeof result[key] === \"object\" &&\n result[key] !== null &&\n !Array.isArray(result[key]) &&\n typeof value === \"object\" &&\n value !== null &&\n !Array.isArray(value)\n ) {\n result[key] = deepMerge(\n result[key] as Record<string, unknown>,\n value as Record<string, unknown>,\n );\n } else {\n result[key] = value;\n }\n }\n return result;\n}\n\n/**\n * Resolve the full configuration for an environment given a config chain.\n *\n * Walks from root (last element) to child (first element), accumulating\n * values via deep merge so that child configs override parent configs.\n *\n * For each config in the chain, base `values` are merged with\n * environment-specific values (env wins), then that result is merged\n * on top of the accumulated parent result (child wins over parent).\n *\n * @param chain - Ordered list of config data from child (index 0) to root ancestor (last).\n * @param environment - The environment key to resolve for.\n */\nexport function resolveChain(chain: ChainConfig[], environment: string): Record<string, unknown> {\n let accumulated: Record<string, unknown> = {};\n\n // Walk from root to child (reverse order — chain is child-to-root)\n for (let i = chain.length - 1; i >= 0; i--) {\n const config = chain[i];\n const baseValues: Record<string, unknown> = config.items ?? {};\n\n // Environments are stored as { env_name: { values: { key: val } } }\n const envEntry = (config.environments ?? {})[environment];\n const envValues: Record<string, unknown> =\n envEntry !== null &&\n envEntry !== undefined &&\n typeof envEntry === \"object\" &&\n !Array.isArray(envEntry)\n ? (((envEntry as Record<string, unknown>).values ?? {}) as Record<string, unknown>)\n : {};\n\n // Merge environment overrides on top of base values (env wins)\n const configResolved = deepMerge(baseValues, envValues);\n\n // Merge this config's resolved values on top of accumulated parent values (child wins)\n accumulated = deepMerge(accumulated, configResolved);\n }\n\n return accumulated;\n}\n"],"mappings":"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","names":[]}
package/dist/index.cjs CHANGED
@@ -49,7 +49,7 @@ function resolveChain(chain, environment) {
49
49
  let accumulated = {};
50
50
  for (let i = chain.length - 1; i >= 0; i--) {
51
51
  const config = chain[i];
52
- const baseValues = config.values ?? {};
52
+ const baseValues = config.items ?? {};
53
53
  const envEntry = (config.environments ?? {})[environment];
54
54
  const envValues = envEntry !== null && envEntry !== void 0 && typeof envEntry === "object" && !Array.isArray(envEntry) ? envEntry.values ?? {} : {};
55
55
  const configResolved = deepMerge(baseValues, envValues);
@@ -318,14 +318,14 @@ var init_runtime = __esm({
318
318
  const envEntry = chainEntry.environments[this._environment] !== void 0 && chainEntry.environments[this._environment] !== null ? chainEntry.environments[this._environment] : null;
319
319
  const envValues = envEntry !== null && typeof envEntry === "object" ? envEntry.values ?? {} : null;
320
320
  if (new_value === null || new_value === void 0) {
321
- delete chainEntry.values[key];
321
+ delete chainEntry.items[key];
322
322
  if (envValues) delete envValues[key];
323
323
  } else if (envValues && key in envValues) {
324
324
  envValues[key] = new_value;
325
- } else if (key in chainEntry.values) {
326
- chainEntry.values[key] = new_value;
325
+ } else if (key in chainEntry.items) {
326
+ chainEntry.items[key] = new_value;
327
327
  } else {
328
- chainEntry.values[key] = new_value;
328
+ chainEntry.items[key] = new_value;
329
329
  }
330
330
  }
331
331
  const oldCache = this._cache;
@@ -438,12 +438,12 @@ var Config = class {
438
438
  description;
439
439
  /** Parent config UUID, or null if this is a root config. */
440
440
  parent;
441
- /** Base key-value pairs. */
442
- values;
441
+ /** Base key-value pairs (unwrapped from typed item definitions). */
442
+ items;
443
443
  /**
444
444
  * Per-environment overrides.
445
- * Stored as `{ env_name: { values: { key: value } } }` to match the
446
- * server's format.
445
+ * Stored as `{ env_name: { values: { key: value } } }` values are
446
+ * unwrapped from the server's `{ value: raw }` wrapper.
447
447
  */
448
448
  environments;
449
449
  /** When the config was created, or null if unavailable. */
@@ -463,7 +463,7 @@ var Config = class {
463
463
  this.name = fields.name;
464
464
  this.description = fields.description;
465
465
  this.parent = fields.parent;
466
- this.values = fields.values;
466
+ this.items = fields.items;
467
467
  this.environments = fields.environments;
468
468
  this.createdAt = fields.createdAt;
469
469
  this.updatedAt = fields.updatedAt;
@@ -476,7 +476,7 @@ var Config = class {
476
476
  *
477
477
  * @param options.name - New display name.
478
478
  * @param options.description - New description (pass empty string to clear).
479
- * @param options.values - New base values (replaces entirely).
479
+ * @param options.items - New base values (replaces entirely).
480
480
  * @param options.environments - New environments dict (replaces entirely).
481
481
  */
482
482
  async update(options) {
@@ -486,12 +486,12 @@ var Config = class {
486
486
  key: this.key,
487
487
  description: options.description !== void 0 ? options.description : this.description,
488
488
  parent: this.parent,
489
- values: options.values ?? this.values,
489
+ items: options.items ?? this.items,
490
490
  environments: options.environments ?? this.environments
491
491
  });
492
492
  this.name = updated.name;
493
493
  this.description = updated.description;
494
- this.values = updated.values;
494
+ this.items = updated.items;
495
495
  this.environments = updated.environments;
496
496
  this.updatedAt = updated.updatedAt;
497
497
  }
@@ -500,19 +500,19 @@ var Config = class {
500
500
  *
501
501
  * When `environment` is provided, replaces that environment's `values`
502
502
  * sub-dict (other environments are preserved). When omitted, replaces
503
- * the base `values`.
503
+ * the base `items`.
504
504
  *
505
505
  * @param values - The complete set of values to set.
506
506
  * @param environment - Target environment, or omit for base values.
507
507
  */
508
508
  async setValues(values, environment) {
509
- let newValues;
509
+ let newItems;
510
510
  let newEnvs;
511
511
  if (environment === void 0) {
512
- newValues = values;
512
+ newItems = values;
513
513
  newEnvs = this.environments;
514
514
  } else {
515
- newValues = this.values;
515
+ newItems = this.items;
516
516
  const existingEntry = typeof this.environments[environment] === "object" && this.environments[environment] !== null ? { ...this.environments[environment] } : {};
517
517
  existingEntry.values = values;
518
518
  newEnvs = { ...this.environments, [environment]: existingEntry };
@@ -523,10 +523,10 @@ var Config = class {
523
523
  key: this.key,
524
524
  description: this.description,
525
525
  parent: this.parent,
526
- values: newValues,
526
+ items: newItems,
527
527
  environments: newEnvs
528
528
  });
529
- this.values = updated.values;
529
+ this.items = updated.items;
530
530
  this.environments = updated.environments;
531
531
  this.updatedAt = updated.updatedAt;
532
532
  }
@@ -541,7 +541,7 @@ var Config = class {
541
541
  */
542
542
  async setValue(key, value, environment) {
543
543
  if (environment === void 0) {
544
- const merged = { ...this.values, [key]: value };
544
+ const merged = { ...this.items, [key]: value };
545
545
  await this.setValues(merged);
546
546
  } else {
547
547
  const envEntry = typeof this.environments[environment] === "object" && this.environments[environment] !== null ? this.environments[environment] : {};
@@ -596,13 +596,13 @@ var Config = class {
596
596
  * @internal
597
597
  */
598
598
  async _buildChain(_timeout) {
599
- const chain = [{ id: this.id, values: this.values, environments: this.environments }];
599
+ const chain = [{ id: this.id, items: this.items, environments: this.environments }];
600
600
  let parentId = this.parent;
601
601
  while (parentId !== null) {
602
602
  const parentConfig = await this._client.get({ id: parentId });
603
603
  chain.push({
604
604
  id: parentConfig.id,
605
- values: parentConfig.values,
605
+ items: parentConfig.items,
606
606
  environments: parentConfig.environments
607
607
  });
608
608
  parentId = parentConfig.parent;
@@ -616,6 +616,30 @@ var Config = class {
616
616
 
617
617
  // src/config/client.ts
618
618
  var BASE_URL = "https://config.smplkit.com";
619
+ function extractItemValues(items) {
620
+ if (!items) return {};
621
+ const result = {};
622
+ for (const [key, item] of Object.entries(items)) {
623
+ result[key] = item && typeof item === "object" && "value" in item ? item.value : item;
624
+ }
625
+ return result;
626
+ }
627
+ function extractEnvironments(environments) {
628
+ if (!environments) return {};
629
+ const result = {};
630
+ for (const [envName, envEntry] of Object.entries(environments)) {
631
+ if (envEntry && typeof envEntry === "object" && envEntry.values) {
632
+ const unwrapped = {};
633
+ for (const [key, item] of Object.entries(envEntry.values)) {
634
+ unwrapped[key] = item && typeof item === "object" && "value" in item ? item.value : item;
635
+ }
636
+ result[envName] = { values: unwrapped };
637
+ } else {
638
+ result[envName] = envEntry;
639
+ }
640
+ }
641
+ return result;
642
+ }
619
643
  function resourceToConfig(resource, client) {
620
644
  const attrs = resource.attributes;
621
645
  return new Config(client, {
@@ -624,8 +648,10 @@ function resourceToConfig(resource, client) {
624
648
  name: attrs.name,
625
649
  description: attrs.description ?? null,
626
650
  parent: attrs.parent ?? null,
627
- values: attrs.values ?? {},
628
- environments: attrs.environments ?? {},
651
+ items: extractItemValues(attrs.items),
652
+ environments: extractEnvironments(
653
+ attrs.environments
654
+ ),
629
655
  createdAt: attrs.created_at ? new Date(attrs.created_at) : null,
630
656
  updatedAt: attrs.updated_at ? new Date(attrs.updated_at) : null
631
657
  });
@@ -654,6 +680,35 @@ function wrapFetchError(err) {
654
680
  `Request failed: ${err instanceof Error ? err.message : String(err)}`
655
681
  );
656
682
  }
683
+ function wrapItemValues(values) {
684
+ if (!values) return null;
685
+ const result = {};
686
+ for (const [key, val] of Object.entries(values)) {
687
+ result[key] = { value: val };
688
+ }
689
+ return result;
690
+ }
691
+ function wrapEnvironments(environments) {
692
+ if (!environments) return null;
693
+ const result = {};
694
+ for (const [envName, envEntry] of Object.entries(environments)) {
695
+ if (envEntry && typeof envEntry === "object" && !Array.isArray(envEntry)) {
696
+ const entry = envEntry;
697
+ if (entry.values && typeof entry.values === "object" && !Array.isArray(entry.values)) {
698
+ const wrapped = {};
699
+ for (const [key, val] of Object.entries(entry.values)) {
700
+ wrapped[key] = { value: val };
701
+ }
702
+ result[envName] = { ...entry, values: wrapped };
703
+ } else {
704
+ result[envName] = envEntry;
705
+ }
706
+ } else {
707
+ result[envName] = envEntry;
708
+ }
709
+ }
710
+ return result;
711
+ }
657
712
  function buildRequestBody(options) {
658
713
  const attrs = {
659
714
  name: options.name
@@ -661,10 +716,10 @@ function buildRequestBody(options) {
661
716
  if (options.key !== void 0) attrs.key = options.key;
662
717
  if (options.description !== void 0) attrs.description = options.description;
663
718
  if (options.parent !== void 0) attrs.parent = options.parent;
664
- if (options.values !== void 0)
665
- attrs.values = options.values;
719
+ if (options.items !== void 0)
720
+ attrs.items = wrapItemValues(options.items);
666
721
  if (options.environments !== void 0)
667
- attrs.environments = options.environments;
722
+ attrs.environments = wrapEnvironments(options.environments);
668
723
  return {
669
724
  data: {
670
725
  id: options.id ?? null,
@@ -747,7 +802,7 @@ var ConfigClient = class {
747
802
  key: options.key,
748
803
  description: options.description,
749
804
  parent: options.parent,
750
- values: options.values
805
+ items: options.items
751
806
  });
752
807
  let data;
753
808
  try {
@@ -790,7 +845,7 @@ var ConfigClient = class {
790
845
  key: payload.key,
791
846
  description: payload.description,
792
847
  parent: payload.parent,
793
- values: payload.values,
848
+ items: payload.items,
794
849
  environments: payload.environments
795
850
  });
796
851
  let data;