How TaskGroup work is setup in SwiftXState

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 an ActorSource you drop into InvokeConfig.src (or spawnChild). The closure is @Sendable (TaskGroupScope) async throws -> [Output].
  • scope.runGroup([...]) — the convenience: takes [@Sendable () async throws -> Output], runs them all concurrently under a withThrowingTaskGroup, and returns results in completion order. It checks cancellation between adds and while collecting.
  • onDone — the result array arrives as event.output; pull it out with event.output?.get([Output].self). Output must be Sendable & 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.

I'm sorry but what benefit or discussion is this intended to foster? It's cool to use coding assistants for your own learning etc, but I don't see how this is a discussion thread for this category.

Related Projects threads came in handy for me when getting started with some of their libraries.

So I guess hopefully—a discussion amongst users where folks can learn the ropes.

PS—Thanks for your work on docc, I've been busy today implementing the more structured version of it into the SwiftXState project. An even better place for tutorials on how to use the APIs. :clinking_beer_mugs: