Skip to content

Commit 8e0ddb0

Browse files
authored
Move AnyAsyncSequenceWorker into WorkflowConcurrency as AsyncSequenceWorker (#353)
This moves `AnyAsyncSequenceWorker` from `WorkflowExperimental` in register into `WorkflowConcurrency` as `AsyncSequenceWorker`. After pairing with @jamieQ I've pushed out an update that includes his change that adds a `Sequence` associated type that can be used to require `Element` from `AsyncSequence` to match `Output`. This allows the worker workflow to be able to get typed output from the `for await`. ## Checklist - [x] Unit Tests - [ ] UI Tests - [ ] Snapshot Tests (iOS only) - [ ] I have made corresponding changes to the documentation
1 parent 53fc18d commit 8e0ddb0

File tree

2 files changed

+502
-0
lines changed

2 files changed

+502
-0
lines changed
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import Foundation
2+
import Workflow
3+
4+
/// Workers define a unit of asynchronous work.
5+
///
6+
/// During a render pass, a workflow can ask the context to await the result of a worker.
7+
///
8+
/// When this occurs, the context checks to see if there is already a running worker of the same type.
9+
/// If there is, and if the workers are 'equivalent', the context leaves the existing worker running.
10+
///
11+
/// If there is not an existing worker of this type, the context will kick off the new worker (via `run`).
12+
public protocol AsyncSequenceWorker<Output>: AnyWorkflowConvertible where Rendering == Void, Output: Sendable {
13+
/// The type of output events returned by this worker.
14+
associatedtype Sequence: AsyncSequence where Sequence.Element == Output
15+
16+
// In iOS 18+ we can do:
17+
// func run() -> any AsyncSequence<Output, Never>
18+
19+
/// Returns an `AsyncSequence` to execute the work represented by this worker.
20+
func run() -> Sequence
21+
22+
/// Returns `true` if the other worker should be considered equivalent to `self`. Equivalence should take into
23+
/// account whatever data is meaningful to the task. For example, a worker that loads a user account from a server
24+
/// would not be equivalent to another worker with a different user ID.
25+
func isEquivalent(to otherWorker: Self) -> Bool
26+
}
27+
28+
extension AsyncSequenceWorker {
29+
public func asAnyWorkflow() -> AnyWorkflow<Void, Output> {
30+
AsyncSequenceWorkerWorkflow(worker: self).asAnyWorkflow()
31+
}
32+
}
33+
34+
struct AsyncSequenceWorkerWorkflow<WorkerType: AsyncSequenceWorker>: Workflow {
35+
let worker: WorkerType
36+
37+
typealias Output = WorkerType.Output
38+
typealias Rendering = Void
39+
typealias State = UUID
40+
41+
func makeInitialState() -> State { UUID() }
42+
43+
func workflowDidChange(from previousWorkflow: AsyncSequenceWorkerWorkflow<WorkerType>, state: inout UUID) {
44+
if !worker.isEquivalent(to: previousWorkflow.worker) {
45+
state = UUID()
46+
}
47+
}
48+
49+
func render(state: State, context: RenderContext<AsyncSequenceWorkerWorkflow>) -> Rendering {
50+
let sink = context.makeSink(of: AnyWorkflowAction.self)
51+
context.runSideEffect(key: state) { lifetime in
52+
let sequence = worker.run()
53+
let task = Task { @MainActor in
54+
for try await output in sequence {
55+
sink.send(AnyWorkflowAction(sendingOutput: output))
56+
}
57+
}
58+
59+
lifetime.onEnded {
60+
task.cancel()
61+
}
62+
}
63+
}
64+
}

0 commit comments

Comments
 (0)