using System; using System.Globalization; using System.Linq; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using RudderSdk.Core.Abstractions; namespace RudderSdk.Core; public sealed partial class ScenarioService { private void DispatchActiveNode(RuntimeRun run, ActiveNodeState state, bool restored) { var node = FindNode(run.Plan, state.NodeId); if (node == null) { run.ActiveNodes.Remove(state.NodeId); CheckRunCompleted(run); return; } var context = new ScenarioNodeContext(this, ToPlanRun(run), node); switch (node.Type ?? string.Empty) { case WaitNode: DispatchWait(run, state, context); break; case RemoteConfigOverrideNode: DispatchRemoteConfigOverride(run, state, context); break; case NotificationNode: EmitNotification(context); break; case StoreNode: EmitStoreOffer(context); break; case QuestNode: EmitQuest(context); break; case LeaderboardNode: EmitLeaderboard(context); break; case BattlePassNode: EmitBattlePass(context); break; case BattlePassLevelNode: EmitBattlePassLevel(context); break; default: // Unsupported node type — fail the run (surfaced via OnScenarioFailed) // instead of leaving it stalled on a node no handler will complete. _client.Options.Logger?.Log(RudderLogLevel.Warning, $"[Rudder] Unsupported scenario node type '{node.Type}' ({node.Id})."); FailRun(run, state.NodeId, new Exception($"Unsupported scenario node type '{node.Type}'")); break; } } private void DispatchWait(RuntimeRun run, ActiveNodeState state, ScenarioNodeContext context) { if (!state.WaitDeadlineUtc.HasValue) { // Prefer server-provided WaitDeadline from the plan boundary over local calculation. // The server stamps WaitDeadline on server-enforced wait boundaries (see StampBoundaries). var boundary = run.Plan.BoundaryNodes?.FirstOrDefault( b => b.SourceNodeId == state.NodeId && b.WaitDeadline.HasValue ); if (boundary?.WaitDeadline is { } parsed) { state.WaitDeadlineUtc = parsed; } else { var delay = GetWaitDelay(context.Data); state.WaitDeadlineUtc = _client.Clock.UtcNow.Add(delay); } Persist(); } var session = new WaitSession(context, state.WaitDeadlineUtc.Value); OnWait?.Invoke(session); if (_client.Clock.UtcNow >= state.WaitDeadlineUtc.Value) _ = CompleteNodeAsync(run.RunId, state.NodeId, "onComplete"); } private void DispatchRemoteConfigOverride(RuntimeRun run, ActiveNodeState state, ScenarioNodeContext context) { var patches = context.Data["patches"] as JArray; if (patches != null) { foreach (var patchToken in patches.OfType()) { var key = patchToken.Value("path"); if (string.IsNullOrEmpty(key)) continue; var valueType = patchToken.Value("valueType") ?? "json"; var value = SerializeRemoteConfigValue(patchToken["value"], valueType); _client.RemoteConfig.ApplyOverride(key, value, valueType); } } var session = new ConfigChangedSession(context); OnConfigChanged?.Invoke(session); _ = CompleteNodeAsync(run.RunId, state.NodeId, "output"); } private void EmitNotification(ScenarioNodeContext context) { OnNotification?.Invoke(new NotificationSession(context)); } private void EmitStoreOffer(ScenarioNodeContext context) { OnStoreOffer?.Invoke(new StoreOfferSession(context)); } private void EmitQuest(ScenarioNodeContext context) { OnQuest?.Invoke(new QuestSession(context)); } private void EmitLeaderboard(ScenarioNodeContext context) { OnLeaderboard?.Invoke(new LeaderboardSession(context)); } private void EmitBattlePass(ScenarioNodeContext context) { OnBattlePass?.Invoke(new BattlePassSession(context, _client.BattlePass)); } private void EmitBattlePassLevel(ScenarioNodeContext context) { OnBattlePassLevel?.Invoke(new BattlePassLevelSession(context)); } private static TimeSpan GetWaitDelay(JObject data) { var duration = data.Value("duration") ?? 0; var unit = data.Value("unit") ?? "seconds"; if (duration <= 0) return TimeSpan.Zero; switch (unit) { case "days": case "day": case "d": return TimeSpan.FromDays(duration); case "hours": case "hour": case "hr": case "h": return TimeSpan.FromHours(duration); case "minutes": case "minute": case "min": case "m": return TimeSpan.FromMinutes(duration); case "seconds": case "second": case "sec": case "s": return TimeSpan.FromSeconds(duration); default: return TimeSpan.FromSeconds(duration); } } private static string? SerializeRemoteConfigValue(JToken? token, string valueType) { if (token == null || token.Type == JTokenType.Null) return null; switch ((valueType ?? string.Empty).ToLowerInvariant()) { case "string": return token.Type == JTokenType.String ? token.Value() : token.ToString(Formatting.None); case "bool": case "boolean": return token.Value().ToString().ToLowerInvariant(); case "int": case "integer": return token.Value().ToString(CultureInfo.InvariantCulture); case "float": case "double": return token.Value().ToString(CultureInfo.InvariantCulture); default: return token.Type == JTokenType.String ? token.Value() : token.ToString(Formatting.None); } } }