Skip to content

Commit e7ec195

Browse files
committed
Move AnyAsyncSequence into WorkflowConcurrency as AsyncSequenceWorker
1 parent 53fc18d commit e7ec195

File tree

2 files changed

+504
-0
lines changed

2 files changed

+504
-0
lines changed
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
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 {
13+
/// The type of output events returned by this worker.
14+
associatedtype Output
15+
16+
// In iOS 18+ we can do:
17+
// func run() -> any AsyncSequence<Output, Never>
18+
// And then remove the casting in the side effect
19+
20+
/// Returns an `AnyAsyncSequence` to execute the work represented by this worker.
21+
func run() -> any AsyncSequence
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 task = Task {
53+
for try await output in worker.run() {
54+
// Not necessary in iOS 18+ once we can use AsyncSequence<Output, Never>
55+
guard let output = output as? Output else {
56+
fatalError("Unexpected output type \(type(of: output)) from worker \(worker)")
57+
}
58+
await sendAction(output: output, sink: sink)
59+
}
60+
}
61+
62+
lifetime.onEnded {
63+
task.cancel()
64+
}
65+
}
66+
}
67+
68+
@MainActor
69+
func sendAction(output: Output, sink: Sink<AnyWorkflowAction<AsyncSequenceWorkerWorkflow<WorkerType>>>) {
70+
sink.send(AnyWorkflowAction(sendingOutput: output))
71+
}
72+
}

0 commit comments

Comments
 (0)