feat(nodes): NatsHookNode — trigger emits one execution per message
This commit is contained in:
86
src/VolcanicCore.Nodes/NatsHookNode.cs
Normal file
86
src/VolcanicCore.Nodes/NatsHookNode.cs
Normal file
@@ -0,0 +1,86 @@
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using NATS.Client.Core;
|
||||
using VolcanicCore.Core;
|
||||
|
||||
namespace VolcanicCore.Nodes;
|
||||
|
||||
public sealed class NatsHookNode : ITriggerNode
|
||||
{
|
||||
// Testable seam over the tiny slice of INatsConnection we actually use.
|
||||
public interface INatsSubscriber
|
||||
{
|
||||
IAsyncEnumerable<byte[]> SubscribeAsync(string subject, CancellationToken ct);
|
||||
}
|
||||
|
||||
private sealed class NatsSubscriberAdapter : INatsSubscriber
|
||||
{
|
||||
private readonly INatsConnection _conn;
|
||||
public NatsSubscriberAdapter(INatsConnection c) => _conn = c;
|
||||
public async IAsyncEnumerable<byte[]> SubscribeAsync(string subject,
|
||||
[System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken ct)
|
||||
{
|
||||
await foreach (var msg in _conn.SubscribeAsync<byte[]>(subject, cancellationToken: ct))
|
||||
if (msg.Data is not null) yield return msg.Data;
|
||||
}
|
||||
}
|
||||
|
||||
public string Id => "nats.hook";
|
||||
public string DisplayName => "NATS Hook";
|
||||
public NodeCategory Category => NodeCategory.Trigger;
|
||||
public int InputCount => 0;
|
||||
public int OutputCount => 1;
|
||||
public string? ConnectionProviderId => "nats";
|
||||
public JsonSchema ConfigSchema { get; } = new(JsonDocument.Parse("""
|
||||
{
|
||||
"type": "object",
|
||||
"required": ["subject"],
|
||||
"properties": { "subject": { "type": "string", "title": "Subject" } }
|
||||
}
|
||||
""").RootElement);
|
||||
|
||||
public Task<IAsyncDisposable> StartAsync(INodeContext ctx, ITriggerSink sink, CancellationToken ct)
|
||||
{
|
||||
var subject = ctx.Config.GetProperty("subject").GetString()!;
|
||||
var raw = ctx.GetConnection<object>();
|
||||
var sub = raw as INatsSubscriber ?? new NatsSubscriberAdapter((INatsConnection)raw);
|
||||
|
||||
var pumpCts = CancellationTokenSource.CreateLinkedTokenSource(ct);
|
||||
var pump = Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
await foreach (var payload in sub.SubscribeAsync(subject, pumpCts.Token))
|
||||
{
|
||||
JsonElement json;
|
||||
try
|
||||
{
|
||||
using var doc = JsonDocument.Parse(payload);
|
||||
json = doc.RootElement.Clone();
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
json = JsonSerializer.SerializeToElement(new { raw = Encoding.UTF8.GetString(payload) });
|
||||
}
|
||||
await sink.EmitAsync(new[] { new Item(json) }, ct: pumpCts.Token);
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException) { /* stop */ }
|
||||
}, pumpCts.Token);
|
||||
|
||||
return Task.FromResult<IAsyncDisposable>(new Handle(pumpCts, pump));
|
||||
}
|
||||
|
||||
private sealed class Handle : IAsyncDisposable
|
||||
{
|
||||
private readonly CancellationTokenSource _cts;
|
||||
private readonly Task _pump;
|
||||
public Handle(CancellationTokenSource c, Task p) { _cts = c; _pump = p; }
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
_cts.Cancel();
|
||||
try { await _pump; } catch { }
|
||||
_cts.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user