Files
rudder-csharp-sdk/Services/Scenarios/ScenarioService.Dispatch.cs
T

191 lines
6.3 KiB
C#
Raw Normal View History

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:
_client.Options.Logger?.Log(RudderLogLevel.Warning, $"[Rudder] Unsupported scenario node type '{node.Type}' ({node.Id}).");
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 && !string.IsNullOrEmpty(b.WaitDeadline)
);
if (boundary != null && DateTimeOffset.TryParse(boundary.WaitDeadline, null, DateTimeStyles.RoundtripKind, out var 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<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)
{
OnBattlePass?.Invoke(new BattlePassSession(context));
}
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);
}
}
}