using System; using System.Collections.Generic; using System.Globalization; using System.Threading; using System.Threading.Tasks; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using RudderSdk.Core.Models.RemoteConfig; namespace RudderSdk.Core; /// /// Remote configuration values. caches every config /// whose "active" flag is not explicitly false; reads /// typed values from the cache. /// public sealed class RemoteConfigService { private readonly RudderClient _client; private readonly Dictionary _cache = new(); private bool _isLoaded; internal RemoteConfigService(RudderClient client) => _client = client; /// True after the first successful . public bool IsLoaded => _isLoaded; /// The cached configs keyed by config key. public IReadOnlyDictionary Configs => _cache; /// Fetches all configs and rebuilds the cache. public async Task> LoadAsync(CancellationToken cancellationToken = default) { var response = await _client.SendAsync( "GET", "/sdk/v1/remote-configs", cancellationToken).ConfigureAwait(false); _cache.Clear(); if (response?.Configs != null) { foreach (var entry in response.Configs) { // Include everything except an explicit "active": false — a missing // flag means active, matching the other SDKs. if (entry.Value?["active"]?.Value() == false) continue; var config = entry.Value?.ToObject(); if (config != null) _cache[config.Key ?? entry.Key] = config; } } _isLoaded = true; return _cache; } /// Fetches one config straight from the server, bypassing the cache. public async Task GetConfigAsync(string key, CancellationToken cancellationToken = default) { return await _client.SendAsync( "GET", "/sdk/v1/remote-configs/" + Url.Encode(key), cancellationToken).ConfigureAwait(false); } /// Reads a typed value from the cache; returns the default when missing or unparsable. public T Get(string key, T defaultValue = default!) { if (!_isLoaded || string.IsNullOrEmpty(key) || !_cache.TryGetValue(key, out var config)) return defaultValue; return ParseValue(config.Value, config.ValueType, defaultValue); } /// Loads the cache on first use, then reads a typed value. public async Task GetAsync(string key, T defaultValue = default!, CancellationToken cancellationToken = default) { if (!_isLoaded) await LoadAsync(cancellationToken).ConfigureAwait(false); return Get(key, defaultValue); } internal void ApplyOverride(string key, string? value, string valueType = "json") { if (string.IsNullOrEmpty(key)) return; _cache[key] = new RemoteConfig { Key = key, // The generated model declares Value non-nullable; a JSON null patch // value is still legal and ParseValue maps it to the caller's default. Value = value!, ValueType = valueType, Active = true }; _isLoaded = true; } private static T ParseValue(string value, string valueType, T defaultValue) { if (value == null) return defaultValue; try { if (typeof(T) == typeof(string)) return (T)(object)value; switch ((valueType ?? string.Empty).ToLowerInvariant()) { case "number": case "int": case "integer": case "float": case "double": return (T)ConvertNumber(value, typeof(T)); case "bool": case "boolean": return (T)(object)bool.Parse(value); case "json": case "object": return JsonConvert.DeserializeObject(value) ?? defaultValue; default: return (T)System.Convert.ChangeType(value, typeof(T), CultureInfo.InvariantCulture); } } catch { return defaultValue; } } private static object ConvertNumber(string value, Type target) { if (target == typeof(float)) return float.Parse(value, CultureInfo.InvariantCulture); if (target == typeof(double)) return double.Parse(value, CultureInfo.InvariantCulture); if (target == typeof(long)) return long.Parse(value, CultureInfo.InvariantCulture); if (target == typeof(decimal)) return decimal.Parse(value, CultureInfo.InvariantCulture); return int.Parse(value, CultureInfo.InvariantCulture); } // Raw shape of the list payload: the generated RemoteConfig.Active is a // non-nullable bool and cannot distinguish "active": false from a missing // flag, so the active check runs on the raw JSON before mapping. private sealed class RawListRemoteConfigsResponse { [JsonProperty("configs")] public Dictionary? Configs { get; set; } } }