Add NATS JetStream publish + trigger activities

NatsPublish (CodeActivity) publishes a UTF-8 body to a subject with optional
Nats-Msg-Id dedup and ExpectedStream assertion. NatsTrigger (Trigger<string>)
starts a workflow per message on a specified stream/subject.

A single BackgroundService scans the trigger store for indexed NatsTriggers,
groups by (stream, subject), and runs one durable pull consumer per pair with
Explicit ack, so multiple Elsa instances share the workload at-least-once. A
mediator handler on WorkflowTriggersIndexed refreshes subscriptions when
workflows are published.

Wired via Nats:Url/User/Password (env-var overrides Nats__*, or short
NATS_URL/NATS_USER/NATS_PASSWORD fallbacks). Uses the NATS.Net v2 client so
wss:// works transparently.
This commit is contained in:
EugeneTes
2026-09-02 09:49:15 +00:00
parent 6ec291c096
commit bd41609424
9 changed files with 427 additions and 0 deletions

View File

@@ -169,6 +169,41 @@ to its DSN.
at create time. `POST /applications/dockercompose` rejects it otherwise.
- Deploys are fire-and-forget — never poll from Claude. User's global rule.
## NATS activities
Three files under `src/Elsa.CustomServer/`:
- `Activities/NatsPublish.cs` — publisher `CodeActivity`. Inputs: `Subject`,
`Body`, optional `MessageId` (sets the `Nats-Msg-Id` header for JetStream
server-side dedup), optional `ExpectedStream` (fail-fast assertion). Outputs
`Sequence` and `Duplicate`.
- `Activities/NatsTrigger.cs` — trigger `Trigger<string>`. Inputs: `Stream`,
`Subject` (wildcards allowed). Outputs: `Result` = message body (UTF-8),
`ReceivedSubject`, `Headers`. Its trigger name / stimulus name is
`NATS.NatsTrigger`.
- `Nats/NatsTriggerHostedService.cs` — one `BackgroundService` that scans
`ITriggerStore` for stored triggers named `NATS.NatsTrigger`, spins up one
durable pull consumer per unique `(stream, subject)` pair, and dispatches
workflows via `IWorkflowDispatcher.DispatchAsync(DispatchTriggerWorkflowsRequest)`
as messages arrive. The mediator handler
`NatsTriggerIndexChangedHandler` re-runs the scan whenever
`WorkflowTriggersIndexed` fires (i.e. after publishing a workflow).
Consumer model: shared durable pull consumer with `DurableName =
elsa-<sanitizedStream>-<sanitizedSubject>` and `AckPolicy.Explicit`. That means
if multiple Elsa instances are running, exactly one processes each message
(load-balanced), at-least-once. `MaxDeliver = 5`, `AckWait = 30s`,
`MaxAckPending = 256`. Failed dispatches NAK with a 5-second delay.
Configuration: `Nats:Url`, `Nats:User`, `Nats:Password` in appsettings
(env-var overrides `Nats__Url`, `Nats__User`, `Nats__Password`, or the
short `NATS_URL` / `NATS_USER` / `NATS_PASSWORD` fallbacks). Leave `Nats:Url`
empty to disable NATS entirely — the activities still register in the toolbox
but publishing throws and no consumers are started.
Packages: `NATS.Net` + `NATS.Client.Hosting` (v2 client). The v1 `NATS.Client`
package does not support `wss://` from .NET — do not use it.
## Session-specific reminders
- Custom activities added under `Activities/` in the SERVER project (not the