2026-08-12 14:04:55 +03:00
|
|
|
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:
|
2026-08-19 17:48:58 +03:00
|
|
|
// Unsupported node type — fail the run (surfaced via OnScenarioFailed)
|
|
|
|
|
// instead of leaving it stalled on a node no handler will complete.
|
2026-08-12 14:04:55 +03:00
|
|
|
_client.Options.Logger?.Log(RudderLogLevel.Warning, $"[Rudder] Unsupported scenario node type '{node.Type}' ({node.Id}).");
|
2026-08-19 17:48:58 +03:00
|
|
|
FailRun(run, state.NodeId, new Exception($"Unsupported scenario node type '{node.Type}'"));
|
2026-08-12 14:04:55 +03:00
|
|
|
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(
|
2026-08-29 11:33:01 +03:00
|
|
|
b => b.SourceNodeId == state.NodeId && b.WaitDeadline.HasValue
|
2026-08-12 14:04:55 +03:00
|
|
|
);
|
2026-08-29 11:33:01 +03:00
|
|
|
if (boundary?.WaitDeadline is { } parsed)
|
2026-08-12 14:04:55 +03:00
|
|
|
{
|
|
|
|
|
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<JObject>())
|
|
|
|
|
{
|
|
|
|
|
var key = patchToken.Value<string>("path");
|
|
|
|
|
if (string.IsNullOrEmpty(key))
|
|
|
|
|
continue;
|
|
|
|
|
|
|
|
|
|
var valueType = patchToken.Value<string>("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)
|
|
|
|
|
{
|
2026-08-19 17:48:58 +03:00
|
|
|
OnBattlePass?.Invoke(new BattlePassSession(context, _client.BattlePass));
|
2026-08-12 14:04:55 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void EmitBattlePassLevel(ScenarioNodeContext context)
|
|
|
|
|
{
|
|
|
|
|
OnBattlePassLevel?.Invoke(new BattlePassLevelSession(context));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static TimeSpan GetWaitDelay(JObject data)
|
|
|
|
|
{
|
|
|
|
|
var duration = data.Value<double?>("duration") ?? 0;
|
|
|
|
|
var unit = data.Value<string>("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<string>() : token.ToString(Formatting.None);
|
|
|
|
|
case "bool":
|
|
|
|
|
case "boolean":
|
|
|
|
|
return token.Value<bool>().ToString().ToLowerInvariant();
|
|
|
|
|
case "int":
|
|
|
|
|
case "integer":
|
|
|
|
|
return token.Value<long>().ToString(CultureInfo.InvariantCulture);
|
|
|
|
|
case "float":
|
|
|
|
|
case "double":
|
|
|
|
|
return token.Value<double>().ToString(CultureInfo.InvariantCulture);
|
|
|
|
|
default:
|
|
|
|
|
return token.Type == JTokenType.String ? token.Value<string>() : token.ToString(Formatting.None);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|