157 lines
5.4 KiB
C#
157 lines
5.4 KiB
C#
using System.Globalization;
|
|
using System.Text.Json.Nodes;
|
|
using w4c_workflows.Models.Nodes;
|
|
|
|
namespace w4c_workflows.Services.Nodes.Executors;
|
|
|
|
/// <summary>
|
|
/// Splitting / loop node. Two behaviours share one node:
|
|
///
|
|
/// <list type="bullet">
|
|
/// <item><b>Chunking</b> (no loop-back edge): runs once with all items and emits
|
|
/// every batch on output 0 plus the original items on output 1 (<c>done</c>).</item>
|
|
/// <item><b>Loop</b> (a loop-back edge targets this node, see
|
|
/// <see cref="NodeBlueprint.LoopBack"/>): emits one batch per invocation on
|
|
/// output 0 and, once the input is exhausted, emits the original items on
|
|
/// output 1 and signals <see cref="NodeExecutionOutcome.LoopComplete"/> so the
|
|
/// runner stops iterating. The cursor lives in the run-scoped state bag, so the
|
|
/// node itself stays stateless.</item>
|
|
/// </list>
|
|
/// </summary>
|
|
public sealed class SplitInBatchesNodeExecutor : INodeExecutor
|
|
{
|
|
public const string DefaultOutputField = "items";
|
|
|
|
public string Type => "core.splitInBatches";
|
|
|
|
public Task<NodeExecutionOutcome> RunAsync(NodeExecutionContext context, CancellationToken ct)
|
|
{
|
|
var batchSize = ReadInt(context, "batchSize", 10);
|
|
if (batchSize < 1)
|
|
return Task.FromResult(NodeExecutionOutcome.Failed("batchSize must be at least 1", "invalid_parameter"));
|
|
|
|
var field = ReadString(context, "outputField");
|
|
if (string.IsNullOrWhiteSpace(field))
|
|
field = DefaultOutputField;
|
|
|
|
if (!context.LoopBackInput)
|
|
{
|
|
var items = context.Input(0);
|
|
return Task.FromResult(Chunk(items, batchSize, field));
|
|
}
|
|
|
|
return Task.FromResult(Loop(context, batchSize, field));
|
|
}
|
|
|
|
/// <summary>Stateless: emit every batch at once and pass all items through to <c>done</c>.</summary>
|
|
private static NodeExecutionOutcome Chunk(IReadOnlyList<FlowItem> items, int batchSize, string field)
|
|
{
|
|
var batches = new List<FlowItem>();
|
|
for (var offset = 0; offset < items.Count; offset += batchSize)
|
|
{
|
|
var batch = items.Skip(offset).Take(batchSize).ToList();
|
|
batches.Add(new FlowItem
|
|
{
|
|
Json = new JsonObject { [field] = BatchJson(batch) },
|
|
Origin = batch[0].Origin,
|
|
});
|
|
}
|
|
|
|
return new NodeExecutionOutcome
|
|
{
|
|
Outputs = new List<IReadOnlyList<FlowItem>> { batches, items.ToList() },
|
|
};
|
|
}
|
|
|
|
/// <summary>Loop mode: one batch per invocation, then a final <c>done</c>.</summary>
|
|
private static NodeExecutionOutcome Loop(NodeExecutionContext context, int batchSize, string field)
|
|
{
|
|
var key = "loop:" + context.TaskId;
|
|
if (context.State.TryGetValue(key, out var existing) && existing is LoopCursor cursor)
|
|
return cursor.Next(field);
|
|
|
|
var seeded = new LoopCursor(context.Input(0).ToList(), batchSize);
|
|
context.State[key] = seeded;
|
|
return seeded.Next(field);
|
|
}
|
|
|
|
private static JsonArray BatchJson(IReadOnlyList<FlowItem> batch)
|
|
{
|
|
var array = new JsonArray();
|
|
foreach (var item in batch)
|
|
array.Add(item.Json.DeepClone());
|
|
return array;
|
|
}
|
|
|
|
/// <summary>Per-run iteration cursor for a loop node.</summary>
|
|
private sealed class LoopCursor
|
|
{
|
|
private readonly int _batchSize;
|
|
|
|
public LoopCursor(List<FlowItem> items, int batchSize)
|
|
{
|
|
Items = items;
|
|
_batchSize = batchSize;
|
|
}
|
|
|
|
public List<FlowItem> Items { get; }
|
|
|
|
public int Offset { get; private set; }
|
|
|
|
public NodeExecutionOutcome Next(string field)
|
|
{
|
|
if (Offset >= Items.Count)
|
|
{
|
|
return new NodeExecutionOutcome
|
|
{
|
|
Outputs = new List<IReadOnlyList<FlowItem>>
|
|
{
|
|
Array.Empty<FlowItem>(),
|
|
Items.ToList(),
|
|
},
|
|
LoopComplete = true,
|
|
};
|
|
}
|
|
|
|
var batch = Items.Skip(Offset).Take(_batchSize).ToList();
|
|
Offset += _batchSize;
|
|
|
|
var batchItem = new FlowItem
|
|
{
|
|
Json = new JsonObject { [field] = BatchJson(batch) },
|
|
Origin = batch[0].Origin,
|
|
};
|
|
|
|
return new NodeExecutionOutcome
|
|
{
|
|
Outputs = new List<IReadOnlyList<FlowItem>>
|
|
{
|
|
new List<FlowItem> { batchItem },
|
|
Array.Empty<FlowItem>(),
|
|
},
|
|
};
|
|
}
|
|
}
|
|
|
|
private static string? ReadString(NodeExecutionContext context, string name)
|
|
=> context.Parameters[name] is JsonValue value && value.TryGetValue<string>(out var text) ? text : null;
|
|
|
|
private static int ReadInt(NodeExecutionContext context, string name, int defaultValue)
|
|
{
|
|
if (context.Parameters[name] is not JsonValue value)
|
|
return defaultValue;
|
|
|
|
if (value.TryGetValue<int>(out var intValue))
|
|
return intValue;
|
|
if (value.TryGetValue<long>(out var longValue))
|
|
return (int)longValue;
|
|
if (value.TryGetValue<decimal>(out var decimalValue))
|
|
return (int)decimalValue;
|
|
if (value.TryGetValue<string>(out var text)
|
|
&& int.TryParse(text, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsed))
|
|
return parsed;
|
|
|
|
return defaultValue;
|
|
}
|
|
}
|