How to setup a state machine node that runs a batch of concurrent TaskGroup work in SwiftXState
Here's how task-group work is set up in SwiftXState. It's an actor logic source (fromTaskGroup) that you invoke like any other actor; it runs concurrent operations, and its onDone hands you the array of results.
The basic shape
"running": StateNodeConfig(
invoke: [
InvokeConfig(
id: "batch",
src: fromTaskGroup { scope in
try await scope.runGroup([
{ @Sendable in try await fetchA() }, // -> Int
{ @Sendable in try await fetchB() },
{ @Sendable in try await fetchC() },
])
},
// onDone fires when ALL operations complete
onDone: .single(TransitionConfig(
target: "done",
actions: [assign { ctx, args in
if let event = args.event as? DoneActorEvent,
let outputs = event.output?.get([Int].self) {
ctx.total = outputs.reduce(0, +)
}
}]
))
),
]
),
"done": StateNodeConfig(type: .final),
The key pieces:
fromTaskGroup { scope in ... }— returns anActorSourceyou drop intoInvokeConfig.src(orspawnChild). The closure is@Sendable (TaskGroupScope) async throws -> [Output].scope.runGroup([...])— the convenience: takes[@Sendable () async throws -> Output], runs them all concurrently under awithThrowingTaskGroup, and returns results in completion order. It checks cancellation between adds and while collecting.onDone— the result array arrives asevent.output; pull it out withevent.output?.get([Output].self).Outputmust beSendable & Equatable.
What TaskGroupScope gives you
public struct TaskGroupScope {
let input: SendableValue? // invoke input, if any
let sendToParent: @Sendable (any Eventable) -> Void // push events back mid-flight
let emit: @Sendable (EmittedEvent) -> Void // emit to subscribers
func runGroup<Output>(_ ops: [...]) async throws -> [Output]
}
So you're not limited to runGroup — you can hand-roll your own withThrowingTaskGroup inside run and use scope.sendToParent(...) to stream partial progress back to the parent machine as each task lands, rather than waiting for the whole batch:
src: fromTaskGroup { scope in
try await withThrowingTaskGroup(of: Int.self) { group in
for url in urls { group.addTask { try await fetch(url) } }
var all: [Int] = []
for try await r in group {
scope.sendToParent(ProgressEvent(value: r)) // live updates
all.append(r)
}
return all
}
}
Cancellation
When the state is exited (or the actor stops), the task group is cancelled. Provide cleanup via the second overload:
fromTaskGroup(
onCancel: { scope in await releaseResources() },
{ scope in try await scope.runGroup([...]) }
)
runGroup already honors Task.checkCancellation() between operations, so in-flight work unwinds cleanly; onCancel is for your own teardown.
So mentally: fromTaskGroup = fromPromise/fromTask, but it fans out to N concurrent operations and its output is the [Output] collection. onDone is the join point. If you want streaming instead of join-only, use sendToParent/emit from inside the scope.