Skip to content

Commit da4e36f

Browse files
authoredNov 17, 2023
Fix potential deadlocks when resuming a continuation while holding a lock (#303)
* Fix potential deadlocks in zip * Fix debounce * Fix combineLatest * Fix Channel * Fix buffer
1 parent 5bbdcc1 commit da4e36f

File tree

6 files changed

+553
-476
lines changed

6 files changed

+553
-476
lines changed
 

‎Sources/AsyncAlgorithms/Buffer/BoundedBufferStorage.swift

+71-58
Original file line numberDiff line numberDiff line change
@@ -18,34 +18,47 @@ final class BoundedBufferStorage<Base: AsyncSequence>: Sendable where Base: Send
1818

1919
func next() async -> Result<Base.Element, Error>? {
2020
return await withTaskCancellationHandler {
21-
let (shouldSuspend, result) = self.stateMachine.withCriticalRegion { stateMachine -> (Bool, Result<Base.Element, Error>?) in
21+
let action: BoundedBufferStateMachine<Base>.NextAction? = self.stateMachine.withCriticalRegion { stateMachine in
2222
let action = stateMachine.next()
2323
switch action {
2424
case .startTask(let base):
2525
self.startTask(stateMachine: &stateMachine, base: base)
26-
return (true, nil)
26+
return nil
27+
2728
case .suspend:
28-
return (true, nil)
29-
case .returnResult(let producerContinuation, let result):
30-
producerContinuation?.resume()
31-
return (false, result)
29+
return action
30+
case .returnResult:
31+
return action
3232
}
3333
}
3434

35-
if !shouldSuspend {
36-
return result
35+
switch action {
36+
case .startTask:
37+
// We are handling the startTask in the lock already because we want to avoid
38+
// other inputs interleaving while starting the task
39+
fatalError("Internal inconsistency")
40+
41+
case .suspend:
42+
break
43+
44+
case .returnResult(let producerContinuation, let result):
45+
producerContinuation?.resume()
46+
return result
47+
48+
case .none:
49+
break
3750
}
3851

3952
return await withUnsafeContinuation { (continuation: UnsafeContinuation<Result<Base.Element, Error>?, Never>) in
40-
self.stateMachine.withCriticalRegion { stateMachine in
41-
let action = stateMachine.nextSuspended(continuation: continuation)
42-
switch action {
43-
case .none:
44-
break
45-
case .returnResult(let producerContinuation, let result):
46-
producerContinuation?.resume()
47-
continuation.resume(returning: result)
48-
}
53+
let action = self.stateMachine.withCriticalRegion { stateMachine in
54+
stateMachine.nextSuspended(continuation: continuation)
55+
}
56+
switch action {
57+
case .none:
58+
break
59+
case .returnResult(let producerContinuation, let result):
60+
producerContinuation?.resume()
61+
continuation.resume(returning: result)
4962
}
5063
}
5164
} onCancel: {
@@ -68,15 +81,15 @@ final class BoundedBufferStorage<Base: AsyncSequence>: Sendable where Base: Send
6881

6982
if shouldSuspend {
7083
await withUnsafeContinuation { (continuation: UnsafeContinuation<Void, Never>) in
71-
self.stateMachine.withCriticalRegion { stateMachine in
72-
let action = stateMachine.producerSuspended(continuation: continuation)
73-
74-
switch action {
75-
case .none:
76-
break
77-
case .resumeProducer:
78-
continuation.resume()
79-
}
84+
let action = self.stateMachine.withCriticalRegion { stateMachine in
85+
stateMachine.producerSuspended(continuation: continuation)
86+
}
87+
88+
switch action {
89+
case .none:
90+
break
91+
case .resumeProducer:
92+
continuation.resume()
8093
}
8194
}
8295
}
@@ -86,35 +99,35 @@ final class BoundedBufferStorage<Base: AsyncSequence>: Sendable where Base: Send
8699
break loop
87100
}
88101

89-
self.stateMachine.withCriticalRegion { stateMachine in
90-
let action = stateMachine.elementProduced(element: element)
91-
switch action {
92-
case .none:
93-
break
94-
case .resumeConsumer(let continuation, let result):
95-
continuation.resume(returning: result)
96-
}
102+
let action = self.stateMachine.withCriticalRegion { stateMachine in
103+
stateMachine.elementProduced(element: element)
97104
}
98-
}
99-
100-
self.stateMachine.withCriticalRegion { stateMachine in
101-
let action = stateMachine.finish(error: nil)
102105
switch action {
103106
case .none:
104107
break
105-
case .resumeConsumer(let continuation):
106-
continuation?.resume(returning: nil)
108+
case .resumeConsumer(let continuation, let result):
109+
continuation.resume(returning: result)
107110
}
108111
}
112+
113+
let action = self.stateMachine.withCriticalRegion { stateMachine in
114+
stateMachine.finish(error: nil)
115+
}
116+
switch action {
117+
case .none:
118+
break
119+
case .resumeConsumer(let continuation):
120+
continuation?.resume(returning: nil)
121+
}
109122
} catch {
110-
self.stateMachine.withCriticalRegion { stateMachine in
111-
let action = stateMachine.finish(error: error)
112-
switch action {
113-
case .none:
114-
break
115-
case .resumeConsumer(let continuation):
116-
continuation?.resume(returning: .failure(error))
117-
}
123+
let action = self.stateMachine.withCriticalRegion { stateMachine in
124+
stateMachine.finish(error: error)
125+
}
126+
switch action {
127+
case .none:
128+
break
129+
case .resumeConsumer(let continuation):
130+
continuation?.resume(returning: .failure(error))
118131
}
119132
}
120133
}
@@ -123,16 +136,16 @@ final class BoundedBufferStorage<Base: AsyncSequence>: Sendable where Base: Send
123136
}
124137

125138
func interrupted() {
126-
self.stateMachine.withCriticalRegion { stateMachine in
127-
let action = stateMachine.interrupted()
128-
switch action {
129-
case .none:
130-
break
131-
case .resumeProducerAndConsumer(let task, let producerContinuation, let consumerContinuation):
132-
task.cancel()
133-
producerContinuation?.resume()
134-
consumerContinuation?.resume(returning: nil)
135-
}
139+
let action = self.stateMachine.withCriticalRegion { stateMachine in
140+
stateMachine.interrupted()
141+
}
142+
switch action {
143+
case .none:
144+
break
145+
case .resumeProducerAndConsumer(let task, let producerContinuation, let consumerContinuation):
146+
task.cancel()
147+
producerContinuation?.resume()
148+
consumerContinuation?.resume(returning: nil)
136149
}
137150
}
138151

‎Sources/AsyncAlgorithms/Buffer/UnboundedBufferStorage.swift

+55-46
Original file line numberDiff line numberDiff line change
@@ -19,32 +19,41 @@ final class UnboundedBufferStorage<Base: AsyncSequence>: Sendable where Base: Se
1919
func next() async -> Result<Base.Element, Error>? {
2020
return await withTaskCancellationHandler {
2121

22-
let (shouldSuspend, result) = self.stateMachine.withCriticalRegion { stateMachine -> (Bool, Result<Base.Element, Error>?) in
22+
let action: UnboundedBufferStateMachine<Base>.NextAction? = self.stateMachine.withCriticalRegion { stateMachine in
2323
let action = stateMachine.next()
2424
switch action {
2525
case .startTask(let base):
2626
self.startTask(stateMachine: &stateMachine, base: base)
27-
return (true, nil)
27+
return nil
2828
case .suspend:
29-
return (true, nil)
30-
case .returnResult(let result):
31-
return (false, result)
29+
return action
30+
case .returnResult:
31+
return action
3232
}
3333
}
3434

35-
if !shouldSuspend {
36-
return result
35+
switch action {
36+
case .startTask:
37+
// We are handling the startTask in the lock already because we want to avoid
38+
// other inputs interleaving while starting the task
39+
fatalError("Internal inconsistency")
40+
case .suspend:
41+
break
42+
case .returnResult(let result):
43+
return result
44+
case .none:
45+
break
3746
}
3847

3948
return await withUnsafeContinuation { (continuation: UnsafeContinuation<Result<Base.Element, Error>?, Never>) in
40-
self.stateMachine.withCriticalRegion { stateMachine in
41-
let action = stateMachine.nextSuspended(continuation: continuation)
42-
switch action {
43-
case .none:
44-
break
45-
case .resumeConsumer(let result):
46-
continuation.resume(returning: result)
47-
}
49+
let action = self.stateMachine.withCriticalRegion { stateMachine in
50+
stateMachine.nextSuspended(continuation: continuation)
51+
}
52+
switch action {
53+
case .none:
54+
break
55+
case .resumeConsumer(let result):
56+
continuation.resume(returning: result)
4857
}
4958
}
5059
} onCancel: {
@@ -59,35 +68,35 @@ final class UnboundedBufferStorage<Base: AsyncSequence>: Sendable where Base: Se
5968
let task = Task {
6069
do {
6170
for try await element in base {
62-
self.stateMachine.withCriticalRegion { stateMachine in
63-
let action = stateMachine.elementProduced(element: element)
64-
switch action {
65-
case .none:
66-
break
67-
case .resumeConsumer(let continuation, let result):
68-
continuation.resume(returning: result)
69-
}
71+
let action = self.stateMachine.withCriticalRegion { stateMachine in
72+
stateMachine.elementProduced(element: element)
7073
}
71-
}
72-
73-
self.stateMachine.withCriticalRegion { stateMachine in
74-
let action = stateMachine.finish(error: nil)
7574
switch action {
7675
case .none:
7776
break
78-
case .resumeConsumer(let continuation):
79-
continuation?.resume(returning: nil)
77+
case .resumeConsumer(let continuation, let result):
78+
continuation.resume(returning: result)
8079
}
8180
}
81+
82+
let action = self.stateMachine.withCriticalRegion { stateMachine in
83+
stateMachine.finish(error: nil)
84+
}
85+
switch action {
86+
case .none:
87+
break
88+
case .resumeConsumer(let continuation):
89+
continuation?.resume(returning: nil)
90+
}
8291
} catch {
83-
self.stateMachine.withCriticalRegion { stateMachine in
84-
let action = stateMachine.finish(error: error)
85-
switch action {
86-
case .none:
87-
break
88-
case .resumeConsumer(let continuation):
89-
continuation?.resume(returning: .failure(error))
90-
}
92+
let action = self.stateMachine.withCriticalRegion { stateMachine in
93+
stateMachine.finish(error: error)
94+
}
95+
switch action {
96+
case .none:
97+
break
98+
case .resumeConsumer(let continuation):
99+
continuation?.resume(returning: .failure(error))
91100
}
92101
}
93102
}
@@ -96,15 +105,15 @@ final class UnboundedBufferStorage<Base: AsyncSequence>: Sendable where Base: Se
96105
}
97106

98107
func interrupted() {
99-
self.stateMachine.withCriticalRegion { stateMachine in
100-
let action = stateMachine.interrupted()
101-
switch action {
102-
case .none:
103-
break
104-
case .resumeConsumer(let task, let continuation):
105-
task.cancel()
106-
continuation?.resume(returning: nil)
107-
}
108+
let action = self.stateMachine.withCriticalRegion { stateMachine in
109+
stateMachine.interrupted()
110+
}
111+
switch action {
112+
case .none:
113+
break
114+
case .resumeConsumer(let task, let continuation):
115+
task.cancel()
116+
continuation?.resume(returning: nil)
108117
}
109118
}
110119

‎Sources/AsyncAlgorithms/Channels/ChannelStorage.swift

+69-76
Original file line numberDiff line numberDiff line change
@@ -25,125 +25,118 @@ struct ChannelStorage<Element: Sendable, Failure: Error>: Sendable {
2525

2626
func send(element: Element) async {
2727
// check if a suspension is needed
28-
let shouldExit = self.stateMachine.withCriticalRegion { stateMachine -> Bool in
29-
let action = stateMachine.send()
30-
31-
switch action {
32-
case .suspend:
33-
// the element has not been delivered because no consumer available, we must suspend
34-
return false
35-
case .resumeConsumer(let continuation):
36-
continuation?.resume(returning: element)
37-
return true
38-
}
28+
let action = self.stateMachine.withCriticalRegion { stateMachine in
29+
stateMachine.send()
3930
}
4031

41-
if shouldExit {
42-
return
32+
switch action {
33+
case .suspend:
34+
break
35+
36+
case .resumeConsumer(let continuation):
37+
continuation?.resume(returning: element)
38+
return
4339
}
4440

4541
let producerID = self.generateId()
4642

4743
await withTaskCancellationHandler {
4844
// a suspension is needed
4945
await withUnsafeContinuation { (continuation: UnsafeContinuation<Void, Never>) in
50-
self.stateMachine.withCriticalRegion { stateMachine in
51-
let action = stateMachine.sendSuspended(continuation: continuation, element: element, producerID: producerID)
52-
53-
switch action {
54-
case .none:
55-
break
56-
case .resumeProducer:
57-
continuation.resume()
58-
case .resumeProducerAndConsumer(let consumerContinuation):
59-
continuation.resume()
60-
consumerContinuation?.resume(returning: element)
61-
}
46+
let action = self.stateMachine.withCriticalRegion { stateMachine in
47+
stateMachine.sendSuspended(continuation: continuation, element: element, producerID: producerID)
6248
}
63-
}
64-
} onCancel: {
65-
self.stateMachine.withCriticalRegion { stateMachine in
66-
let action = stateMachine.sendCancelled(producerID: producerID)
6749

6850
switch action {
6951
case .none:
7052
break
71-
case .resumeProducer(let continuation):
72-
continuation?.resume()
53+
case .resumeProducer:
54+
continuation.resume()
55+
case .resumeProducerAndConsumer(let consumerContinuation):
56+
continuation.resume()
57+
consumerContinuation?.resume(returning: element)
7358
}
7459
}
75-
}
76-
}
77-
78-
func finish(error: Failure? = nil) {
79-
self.stateMachine.withCriticalRegion { stateMachine in
80-
let action = stateMachine.finish(error: error)
60+
} onCancel: {
61+
let action = self.stateMachine.withCriticalRegion { stateMachine in
62+
stateMachine.sendCancelled(producerID: producerID)
63+
}
8164

8265
switch action {
8366
case .none:
8467
break
85-
case .resumeProducersAndConsumers(let producerContinuations, let consumerContinuations):
86-
producerContinuations.forEach { $0?.resume() }
87-
if let error {
88-
consumerContinuations.forEach { $0?.resume(throwing: error) }
89-
} else {
90-
consumerContinuations.forEach { $0?.resume(returning: nil) }
91-
}
68+
case .resumeProducer(let continuation):
69+
continuation?.resume()
9270
}
9371
}
9472
}
9573

96-
func next() async throws -> Element? {
97-
let (shouldExit, result) = self.stateMachine.withCriticalRegion { stateMachine -> (Bool, Result<Element?, Error>?) in
98-
let action = stateMachine.next()
74+
func finish(error: Failure? = nil) {
75+
let action = self.stateMachine.withCriticalRegion { stateMachine in
76+
stateMachine.finish(error: error)
77+
}
9978

100-
switch action {
101-
case .suspend:
102-
return (false, nil)
103-
case .resumeProducer(let producerContinuation, let result):
104-
producerContinuation?.resume()
105-
return (true, result)
106-
}
79+
switch action {
80+
case .none:
81+
break
82+
case .resumeProducersAndConsumers(let producerContinuations, let consumerContinuations):
83+
producerContinuations.forEach { $0?.resume() }
84+
if let error {
85+
consumerContinuations.forEach { $0?.resume(throwing: error) }
86+
} else {
87+
consumerContinuations.forEach { $0?.resume(returning: nil) }
88+
}
10789
}
90+
}
10891

109-
if shouldExit {
110-
return try result?._rethrowGet()
92+
func next() async throws -> Element? {
93+
let action = self.stateMachine.withCriticalRegion { stateMachine in
94+
stateMachine.next()
95+
}
96+
97+
switch action {
98+
case .suspend:
99+
break
100+
101+
case .resumeProducer(let producerContinuation, let result):
102+
producerContinuation?.resume()
103+
return try result._rethrowGet()
111104
}
112105

113106
let consumerID = self.generateId()
114107

115108
return try await withTaskCancellationHandler {
116109
try await withUnsafeThrowingContinuation { (continuation: UnsafeContinuation<Element?, any Error>) in
117-
self.stateMachine.withCriticalRegion { stateMachine in
118-
let action = stateMachine.nextSuspended(
110+
let action = self.stateMachine.withCriticalRegion { stateMachine in
111+
stateMachine.nextSuspended(
119112
continuation: continuation,
120113
consumerID: consumerID
121114
)
122-
123-
switch action {
124-
case .none:
125-
break
126-
case .resumeConsumer(let element):
127-
continuation.resume(returning: element)
128-
case .resumeConsumerWithError(let error):
129-
continuation.resume(throwing: error)
130-
case .resumeProducerAndConsumer(let producerContinuation, let element):
131-
producerContinuation?.resume()
132-
continuation.resume(returning: element)
133-
}
134115
}
135-
}
136-
} onCancel: {
137-
self.stateMachine.withCriticalRegion { stateMachine in
138-
let action = stateMachine.nextCancelled(consumerID: consumerID)
139116

140117
switch action {
141118
case .none:
142119
break
143-
case .resumeConsumer(let continuation):
144-
continuation?.resume(returning: nil)
120+
case .resumeConsumer(let element):
121+
continuation.resume(returning: element)
122+
case .resumeConsumerWithError(let error):
123+
continuation.resume(throwing: error)
124+
case .resumeProducerAndConsumer(let producerContinuation, let element):
125+
producerContinuation?.resume()
126+
continuation.resume(returning: element)
145127
}
146128
}
129+
} onCancel: {
130+
let action = self.stateMachine.withCriticalRegion { stateMachine in
131+
stateMachine.nextCancelled(consumerID: consumerID)
132+
}
133+
134+
switch action {
135+
case .none:
136+
break
137+
case .resumeConsumer(let continuation):
138+
continuation?.resume(returning: nil)
139+
}
147140
}
148141
}
149142
}

‎Sources/AsyncAlgorithms/CombineLatest/CombineLatestStorage.swift

+122-101
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ final class CombineLatestStorage<
4848
func next() async rethrows -> (Base1.Element, Base2.Element, Base3.Element?)? {
4949
try await withTaskCancellationHandler {
5050
let result = await withUnsafeContinuation { continuation in
51-
self.stateMachine.withCriticalRegion { stateMachine in
51+
let action: StateMachine.NextAction? = self.stateMachine.withCriticalRegion { stateMachine in
5252
let action = stateMachine.next(for: continuation)
5353
switch action {
5454
case .startTask(let base1, let base2, let base3):
@@ -60,45 +60,65 @@ final class CombineLatestStorage<
6060
base3: base3,
6161
downstreamContinuation: continuation
6262
)
63+
return nil
6364

64-
case .resumeContinuation(let downstreamContinuation, let result):
65-
downstreamContinuation.resume(returning: result)
65+
case .resumeContinuation:
66+
return action
6667

67-
case .resumeUpstreamContinuations(let upstreamContinuations):
68-
// bases can be iterated over for 1 iteration so their next value can be retrieved
69-
upstreamContinuations.forEach { $0.resume() }
68+
case .resumeUpstreamContinuations:
69+
return action
7070

71-
case .resumeDownstreamContinuationWithNil(let continuation):
72-
// the async sequence is already finished, immediately resuming
73-
continuation.resume(returning: .success(nil))
71+
case .resumeDownstreamContinuationWithNil:
72+
return action
7473
}
7574
}
75+
76+
switch action {
77+
case .startTask:
78+
// We are handling the startTask in the lock already because we want to avoid
79+
// other inputs interleaving while starting the task
80+
fatalError("Internal inconsistency")
81+
82+
case .resumeContinuation(let downstreamContinuation, let result):
83+
downstreamContinuation.resume(returning: result)
84+
85+
case .resumeUpstreamContinuations(let upstreamContinuations):
86+
// bases can be iterated over for 1 iteration so their next value can be retrieved
87+
upstreamContinuations.forEach { $0.resume() }
88+
89+
case .resumeDownstreamContinuationWithNil(let continuation):
90+
// the async sequence is already finished, immediately resuming
91+
continuation.resume(returning: .success(nil))
92+
93+
case .none:
94+
break
95+
}
7696
}
7797

7898
return try result._rethrowGet()
7999

80100
} onCancel: {
81-
self.stateMachine.withCriticalRegion { stateMachine in
82-
let action = stateMachine.cancelled()
101+
let action = self.stateMachine.withCriticalRegion { stateMachine in
102+
stateMachine.cancelled()
103+
}
83104

84-
switch action {
85-
case .resumeDownstreamContinuationWithNilAndCancelTaskAndUpstreamContinuations(
86-
let downstreamContinuation,
87-
let task,
88-
let upstreamContinuations
89-
):
90-
upstreamContinuations.forEach { $0.resume(throwing: CancellationError()) }
91-
task.cancel()
105+
switch action {
106+
case .resumeDownstreamContinuationWithNilAndCancelTaskAndUpstreamContinuations(
107+
let downstreamContinuation,
108+
let task,
109+
let upstreamContinuations
110+
):
111+
upstreamContinuations.forEach { $0.resume(throwing: CancellationError()) }
112+
task.cancel()
92113

93-
downstreamContinuation.resume(returning: .success(nil))
114+
downstreamContinuation.resume(returning: .success(nil))
94115

95-
case .cancelTaskAndUpstreamContinuations(let task, let upstreamContinuations):
96-
upstreamContinuations.forEach { $0.resume(throwing: CancellationError()) }
97-
task.cancel()
116+
case .cancelTaskAndUpstreamContinuations(let task, let upstreamContinuations):
117+
upstreamContinuations.forEach { $0.resume(throwing: CancellationError()) }
118+
task.cancel()
98119

99-
case .none:
100-
break
101-
}
120+
case .none:
121+
break
102122
}
103123
}
104124
}
@@ -124,33 +144,33 @@ final class CombineLatestStorage<
124144
// element from upstream. This continuation is only resumed
125145
// if the downstream consumer called `next` to signal his demand.
126146
try await withUnsafeThrowingContinuation { continuation in
127-
self.stateMachine.withCriticalRegion { stateMachine in
128-
let action = stateMachine.childTaskSuspended(baseIndex: 0, continuation: continuation)
147+
let action = self.stateMachine.withCriticalRegion { stateMachine in
148+
stateMachine.childTaskSuspended(baseIndex: 0, continuation: continuation)
149+
}
129150

130-
switch action {
131-
case .resumeContinuation(let upstreamContinuation):
132-
upstreamContinuation.resume()
151+
switch action {
152+
case .resumeContinuation(let upstreamContinuation):
153+
upstreamContinuation.resume()
133154

134-
case .resumeContinuationWithError(let upstreamContinuation, let error):
135-
upstreamContinuation.resume(throwing: error)
155+
case .resumeContinuationWithError(let upstreamContinuation, let error):
156+
upstreamContinuation.resume(throwing: error)
136157

137-
case .none:
138-
break
139-
}
158+
case .none:
159+
break
140160
}
141161
}
142162

143163
if let element1 = try await base1Iterator.next() {
144-
self.stateMachine.withCriticalRegion { stateMachine in
145-
let action = stateMachine.elementProduced((element1, nil, nil))
164+
let action = self.stateMachine.withCriticalRegion { stateMachine in
165+
stateMachine.elementProduced((element1, nil, nil))
166+
}
146167

147-
switch action {
148-
case .resumeContinuation(let downstreamContinuation, let result):
149-
downstreamContinuation.resume(returning: result)
168+
switch action {
169+
case .resumeContinuation(let downstreamContinuation, let result):
170+
downstreamContinuation.resume(returning: result)
150171

151-
case .none:
152-
break
153-
}
172+
case .none:
173+
break
154174
}
155175
} else {
156176
let action = self.stateMachine.withCriticalRegion { stateMachine in
@@ -191,33 +211,33 @@ final class CombineLatestStorage<
191211
// element from upstream. This continuation is only resumed
192212
// if the downstream consumer called `next` to signal his demand.
193213
try await withUnsafeThrowingContinuation { continuation in
194-
self.stateMachine.withCriticalRegion { stateMachine in
195-
let action = stateMachine.childTaskSuspended(baseIndex: 1, continuation: continuation)
214+
let action = self.stateMachine.withCriticalRegion { stateMachine in
215+
stateMachine.childTaskSuspended(baseIndex: 1, continuation: continuation)
216+
}
196217

197-
switch action {
198-
case .resumeContinuation(let upstreamContinuation):
199-
upstreamContinuation.resume()
218+
switch action {
219+
case .resumeContinuation(let upstreamContinuation):
220+
upstreamContinuation.resume()
200221

201-
case .resumeContinuationWithError(let upstreamContinuation, let error):
202-
upstreamContinuation.resume(throwing: error)
222+
case .resumeContinuationWithError(let upstreamContinuation, let error):
223+
upstreamContinuation.resume(throwing: error)
203224

204-
case .none:
205-
break
206-
}
225+
case .none:
226+
break
207227
}
208228
}
209229

210230
if let element2 = try await base1Iterator.next() {
211-
self.stateMachine.withCriticalRegion { stateMachine in
212-
let action = stateMachine.elementProduced((nil, element2, nil))
231+
let action = self.stateMachine.withCriticalRegion { stateMachine in
232+
stateMachine.elementProduced((nil, element2, nil))
233+
}
213234

214-
switch action {
215-
case .resumeContinuation(let downstreamContinuation, let result):
216-
downstreamContinuation.resume(returning: result)
235+
switch action {
236+
case .resumeContinuation(let downstreamContinuation, let result):
237+
downstreamContinuation.resume(returning: result)
217238

218-
case .none:
219-
break
220-
}
239+
case .none:
240+
break
221241
}
222242
} else {
223243
let action = self.stateMachine.withCriticalRegion { stateMachine in
@@ -259,33 +279,33 @@ final class CombineLatestStorage<
259279
// element from upstream. This continuation is only resumed
260280
// if the downstream consumer called `next` to signal his demand.
261281
try await withUnsafeThrowingContinuation { continuation in
262-
self.stateMachine.withCriticalRegion { stateMachine in
263-
let action = stateMachine.childTaskSuspended(baseIndex: 2, continuation: continuation)
282+
let action = self.stateMachine.withCriticalRegion { stateMachine in
283+
stateMachine.childTaskSuspended(baseIndex: 2, continuation: continuation)
284+
}
264285

265-
switch action {
266-
case .resumeContinuation(let upstreamContinuation):
267-
upstreamContinuation.resume()
286+
switch action {
287+
case .resumeContinuation(let upstreamContinuation):
288+
upstreamContinuation.resume()
268289

269-
case .resumeContinuationWithError(let upstreamContinuation, let error):
270-
upstreamContinuation.resume(throwing: error)
290+
case .resumeContinuationWithError(let upstreamContinuation, let error):
291+
upstreamContinuation.resume(throwing: error)
271292

272-
case .none:
273-
break
274-
}
293+
case .none:
294+
break
275295
}
276296
}
277297

278298
if let element3 = try await base1Iterator.next() {
279-
self.stateMachine.withCriticalRegion { stateMachine in
280-
let action = stateMachine.elementProduced((nil, nil, element3))
299+
let action = self.stateMachine.withCriticalRegion { stateMachine in
300+
stateMachine.elementProduced((nil, nil, element3))
301+
}
281302

282-
switch action {
283-
case .resumeContinuation(let downstreamContinuation, let result):
284-
downstreamContinuation.resume(returning: result)
303+
switch action {
304+
case .resumeContinuation(let downstreamContinuation, let result):
305+
downstreamContinuation.resume(returning: result)
285306

286-
case .none:
287-
break
288-
}
307+
case .none:
308+
break
289309
}
290310
} else {
291311
let action = self.stateMachine.withCriticalRegion { stateMachine in
@@ -323,28 +343,29 @@ final class CombineLatestStorage<
323343
do {
324344
try await group.next()
325345
} catch {
326-
// One of the upstream sequences threw an error
327-
self.stateMachine.withCriticalRegion { stateMachine in
328-
let action = stateMachine.upstreamThrew(error)
329-
switch action {
330-
case .cancelTaskAndUpstreamContinuations(let task, let upstreamContinuations):
331-
upstreamContinuations.forEach { $0.resume(throwing: CancellationError()) }
332-
task.cancel()
333-
case .resumeContinuationWithErrorAndCancelTaskAndUpstreamContinuations(
334-
let downstreamContinuation,
335-
let error,
336-
let task,
337-
let upstreamContinuations
338-
):
339-
upstreamContinuations.forEach { $0.resume(throwing: CancellationError()) }
340-
task.cancel()
341-
downstreamContinuation.resume(returning: .failure(error))
342-
case .none:
343-
break
344-
}
345-
}
346+
// One of the upstream sequences threw an error
347+
let action = self.stateMachine.withCriticalRegion { stateMachine in
348+
stateMachine.upstreamThrew(error)
349+
}
350+
351+
switch action {
352+
case .cancelTaskAndUpstreamContinuations(let task, let upstreamContinuations):
353+
upstreamContinuations.forEach { $0.resume(throwing: CancellationError()) }
354+
task.cancel()
355+
case .resumeContinuationWithErrorAndCancelTaskAndUpstreamContinuations(
356+
let downstreamContinuation,
357+
let error,
358+
let task,
359+
let upstreamContinuations
360+
):
361+
upstreamContinuations.forEach { $0.resume(throwing: CancellationError()) }
362+
task.cancel()
363+
downstreamContinuation.resume(returning: .failure(error))
364+
case .none:
365+
break
366+
}
346367

347-
group.cancelAll()
368+
group.cancelAll()
348369
}
349370
}
350371
}

‎Sources/AsyncAlgorithms/Debounce/DebounceStorage.swift

+81-57
Original file line numberDiff line numberDiff line change
@@ -55,36 +55,59 @@ final class DebounceStorage<Base: AsyncSequence & Sendable, C: Clock>: Sendable
5555
// We always suspend since we can never return an element right away
5656

5757
let result: Result<Element?, Error> = await withUnsafeContinuation { continuation in
58-
self.stateMachine.withCriticalRegion {
59-
let action = $0.next(for: continuation)
60-
61-
switch action {
62-
case .startTask(let base):
63-
self.startTask(
64-
stateMachine: &$0,
65-
base: base,
66-
downstreamContinuation: continuation
67-
)
68-
69-
case .resumeUpstreamContinuation(let upstreamContinuation):
70-
// This is signalling the upstream task that is consuming the upstream
71-
// sequence to signal demand.
72-
upstreamContinuation?.resume(returning: ())
73-
74-
case .resumeUpstreamAndClockContinuation(let upstreamContinuation, let clockContinuation, let deadline):
75-
// This is signalling the upstream task that is consuming the upstream
76-
// sequence to signal demand and start the clock task.
77-
upstreamContinuation?.resume(returning: ())
78-
clockContinuation?.resume(returning: deadline)
79-
80-
case .resumeDownstreamContinuationWithNil(let continuation):
81-
continuation.resume(returning: .success(nil))
82-
83-
case .resumeDownstreamContinuationWithError(let continuation, let error):
84-
continuation.resume(returning: .failure(error))
85-
}
58+
let action: DebounceStateMachine<Base, C>.NextAction? = self.stateMachine.withCriticalRegion {
59+
let action = $0.next(for: continuation)
60+
61+
switch action {
62+
case .startTask(let base):
63+
self.startTask(
64+
stateMachine: &$0,
65+
base: base,
66+
downstreamContinuation: continuation
67+
)
68+
return nil
69+
70+
case .resumeUpstreamContinuation:
71+
return action
72+
73+
case .resumeUpstreamAndClockContinuation:
74+
return action
75+
76+
case .resumeDownstreamContinuationWithNil:
77+
return action
78+
79+
case .resumeDownstreamContinuationWithError:
80+
return action
8681
}
87-
}
82+
}
83+
84+
switch action {
85+
case .startTask:
86+
// We are handling the startTask in the lock already because we want to avoid
87+
// other inputs interleaving while starting the task
88+
fatalError("Internal inconsistency")
89+
90+
case .resumeUpstreamContinuation(let upstreamContinuation):
91+
// This is signalling the upstream task that is consuming the upstream
92+
// sequence to signal demand.
93+
upstreamContinuation?.resume(returning: ())
94+
95+
case .resumeUpstreamAndClockContinuation(let upstreamContinuation, let clockContinuation, let deadline):
96+
// This is signalling the upstream task that is consuming the upstream
97+
// sequence to signal demand and start the clock task.
98+
upstreamContinuation?.resume(returning: ())
99+
clockContinuation?.resume(returning: deadline)
100+
101+
case .resumeDownstreamContinuationWithNil(let continuation):
102+
continuation.resume(returning: .success(nil))
103+
104+
case .resumeDownstreamContinuationWithError(let continuation, let error):
105+
continuation.resume(returning: .failure(error))
106+
107+
case .none:
108+
break
109+
}
110+
}
88111

89112
return try result._rethrowGet()
90113
} onCancel: {
@@ -258,37 +281,38 @@ final class DebounceStorage<Base: AsyncSequence & Sendable, C: Clock>: Sendable
258281
do {
259282
try await group.next()
260283
} catch {
261-
// One of the upstream sequences threw an error
262-
self.stateMachine.withCriticalRegion { stateMachine in
263-
let action = stateMachine.upstreamThrew(error)
264-
switch action {
265-
case .resumeContinuationWithErrorAndCancelTaskAndUpstreamContinuation(
266-
let downstreamContinuation,
267-
let error,
268-
let task,
269-
let upstreamContinuation,
270-
let clockContinuation
271-
):
272-
upstreamContinuation?.resume(throwing: CancellationError())
273-
clockContinuation?.resume(throwing: CancellationError())
274-
275-
task.cancel()
276-
277-
downstreamContinuation.resume(returning: .failure(error))
278-
279-
case .cancelTaskAndClockContinuation(
280-
let task,
281-
let clockContinuation
282-
):
283-
clockContinuation?.resume(throwing: CancellationError())
284-
task.cancel()
285-
case .none:
286-
break
287-
}
284+
// One of the upstream sequences threw an error
285+
let action = self.stateMachine.withCriticalRegion { stateMachine in
286+
stateMachine.upstreamThrew(error)
288287
}
289288

290-
group.cancelAll()
289+
switch action {
290+
case .resumeContinuationWithErrorAndCancelTaskAndUpstreamContinuation(
291+
let downstreamContinuation,
292+
let error,
293+
let task,
294+
let upstreamContinuation,
295+
let clockContinuation
296+
):
297+
upstreamContinuation?.resume(throwing: CancellationError())
298+
clockContinuation?.resume(throwing: CancellationError())
299+
300+
task.cancel()
301+
302+
downstreamContinuation.resume(returning: .failure(error))
303+
304+
case .cancelTaskAndClockContinuation(
305+
let task,
306+
let clockContinuation
307+
):
308+
clockContinuation?.resume(throwing: CancellationError())
309+
task.cancel()
310+
case .none:
311+
break
312+
}
291313
}
314+
315+
group.cancelAll()
292316
}
293317
}
294318
}

‎Sources/AsyncAlgorithms/Zip/ZipStorage.swift

+155-138
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ final class ZipStorage<Base1: AsyncSequence, Base2: AsyncSequence, Base3: AsyncS
3939
func next() async rethrows -> (Base1.Element, Base2.Element, Base3.Element?)? {
4040
try await withTaskCancellationHandler {
4141
let result = await withUnsafeContinuation { continuation in
42-
self.stateMachine.withCriticalRegion { stateMachine in
42+
let action: StateMachine.NextAction? = self.stateMachine.withCriticalRegion { stateMachine in
4343
let action = stateMachine.next(for: continuation)
4444
switch action {
4545
case .startTask(let base1, let base2, let base3):
@@ -51,42 +51,59 @@ final class ZipStorage<Base1: AsyncSequence, Base2: AsyncSequence, Base3: AsyncS
5151
base3: base3,
5252
downstreamContinuation: continuation
5353
)
54+
return nil
5455

55-
case .resumeUpstreamContinuations(let upstreamContinuations):
56-
// bases can be iterated over for 1 iteration so their next value can be retrieved
57-
upstreamContinuations.forEach { $0.resume() }
56+
case .resumeUpstreamContinuations:
57+
return action
5858

59-
case .resumeDownstreamContinuationWithNil(let continuation):
60-
// the async sequence is already finished, immediately resuming
61-
continuation.resume(returning: .success(nil))
59+
case .resumeDownstreamContinuationWithNil:
60+
return action
6261
}
6362
}
63+
64+
switch action {
65+
case .startTask:
66+
// We are handling the startTask in the lock already because we want to avoid
67+
// other inputs interleaving while starting the task
68+
fatalError("Internal inconsistency")
69+
70+
case .resumeUpstreamContinuations(let upstreamContinuations):
71+
// bases can be iterated over for 1 iteration so their next value can be retrieved
72+
upstreamContinuations.forEach { $0.resume() }
73+
74+
case .resumeDownstreamContinuationWithNil(let continuation):
75+
// the async sequence is already finished, immediately resuming
76+
continuation.resume(returning: .success(nil))
77+
78+
case .none:
79+
break
80+
}
6481
}
6582

6683
return try result._rethrowGet()
6784

6885
} onCancel: {
69-
self.stateMachine.withCriticalRegion { stateMachine in
70-
let action = stateMachine.cancelled()
86+
let action = self.stateMachine.withCriticalRegion { stateMachine in
87+
stateMachine.cancelled()
88+
}
7189

72-
switch action {
73-
case .resumeDownstreamContinuationWithNilAndCancelTaskAndUpstreamContinuations(
74-
let downstreamContinuation,
75-
let task,
76-
let upstreamContinuations
77-
):
78-
upstreamContinuations.forEach { $0.resume(throwing: CancellationError()) }
79-
task.cancel()
90+
switch action {
91+
case .resumeDownstreamContinuationWithNilAndCancelTaskAndUpstreamContinuations(
92+
let downstreamContinuation,
93+
let task,
94+
let upstreamContinuations
95+
):
96+
upstreamContinuations.forEach { $0.resume(throwing: CancellationError()) }
97+
task.cancel()
8098

81-
downstreamContinuation.resume(returning: .success(nil))
99+
downstreamContinuation.resume(returning: .success(nil))
82100

83-
case .cancelTaskAndUpstreamContinuations(let task, let upstreamContinuations):
84-
upstreamContinuations.forEach { $0.resume(throwing: CancellationError()) }
85-
task.cancel()
101+
case .cancelTaskAndUpstreamContinuations(let task, let upstreamContinuations):
102+
upstreamContinuations.forEach { $0.resume(throwing: CancellationError()) }
103+
task.cancel()
86104

87-
case .none:
88-
break
89-
}
105+
case .none:
106+
break
90107
}
91108
}
92109
}
@@ -112,53 +129,53 @@ final class ZipStorage<Base1: AsyncSequence, Base2: AsyncSequence, Base3: AsyncS
112129
// element from upstream. This continuation is only resumed
113130
// if the downstream consumer called `next` to signal his demand.
114131
try await withUnsafeThrowingContinuation { continuation in
115-
self.stateMachine.withCriticalRegion { stateMachine in
116-
let action = stateMachine.childTaskSuspended(baseIndex: 0, continuation: continuation)
132+
let action = self.stateMachine.withCriticalRegion { stateMachine in
133+
stateMachine.childTaskSuspended(baseIndex: 0, continuation: continuation)
134+
}
117135

118-
switch action {
119-
case .resumeContinuation(let upstreamContinuation):
120-
upstreamContinuation.resume()
136+
switch action {
137+
case .resumeContinuation(let upstreamContinuation):
138+
upstreamContinuation.resume()
121139

122-
case .resumeContinuationWithError(let upstreamContinuation, let error):
123-
upstreamContinuation.resume(throwing: error)
140+
case .resumeContinuationWithError(let upstreamContinuation, let error):
141+
upstreamContinuation.resume(throwing: error)
124142

125-
case .none:
126-
break
127-
}
143+
case .none:
144+
break
128145
}
129146
}
130147

131148
if let element1 = try await base1Iterator.next() {
132-
self.stateMachine.withCriticalRegion { stateMachine in
133-
let action = stateMachine.elementProduced((element1, nil, nil))
149+
let action = self.stateMachine.withCriticalRegion { stateMachine in
150+
stateMachine.elementProduced((element1, nil, nil))
151+
}
134152

135-
switch action {
136-
case .resumeContinuation(let downstreamContinuation, let result):
137-
downstreamContinuation.resume(returning: result)
153+
switch action {
154+
case .resumeContinuation(let downstreamContinuation, let result):
155+
downstreamContinuation.resume(returning: result)
138156

139-
case .none:
140-
break
141-
}
157+
case .none:
158+
break
142159
}
143160
} else {
144-
self.stateMachine.withCriticalRegion { stateMachine in
145-
let action = stateMachine.upstreamFinished()
161+
let action = self.stateMachine.withCriticalRegion { stateMachine in
162+
stateMachine.upstreamFinished()
163+
}
146164

147-
switch action {
148-
case .resumeContinuationWithNilAndCancelTaskAndUpstreamContinuations(
149-
let downstreamContinuation,
150-
let task,
151-
let upstreamContinuations
152-
):
165+
switch action {
166+
case .resumeContinuationWithNilAndCancelTaskAndUpstreamContinuations(
167+
let downstreamContinuation,
168+
let task,
169+
let upstreamContinuations
170+
):
153171

154-
upstreamContinuations.forEach { $0.resume(throwing: CancellationError()) }
155-
task.cancel()
172+
upstreamContinuations.forEach { $0.resume(throwing: CancellationError()) }
173+
task.cancel()
156174

157-
downstreamContinuation.resume(returning: .success(nil))
175+
downstreamContinuation.resume(returning: .success(nil))
158176

159-
case .none:
160-
break
161-
}
177+
case .none:
178+
break
162179
}
163180
}
164181
}
@@ -172,53 +189,53 @@ final class ZipStorage<Base1: AsyncSequence, Base2: AsyncSequence, Base3: AsyncS
172189
// element from upstream. This continuation is only resumed
173190
// if the downstream consumer called `next` to signal his demand.
174191
try await withUnsafeThrowingContinuation { continuation in
175-
self.stateMachine.withCriticalRegion { stateMachine in
176-
let action = stateMachine.childTaskSuspended(baseIndex: 1, continuation: continuation)
192+
let action = self.stateMachine.withCriticalRegion { stateMachine in
193+
stateMachine.childTaskSuspended(baseIndex: 1, continuation: continuation)
194+
}
177195

178-
switch action {
179-
case .resumeContinuation(let upstreamContinuation):
180-
upstreamContinuation.resume()
196+
switch action {
197+
case .resumeContinuation(let upstreamContinuation):
198+
upstreamContinuation.resume()
181199

182-
case .resumeContinuationWithError(let upstreamContinuation, let error):
183-
upstreamContinuation.resume(throwing: error)
200+
case .resumeContinuationWithError(let upstreamContinuation, let error):
201+
upstreamContinuation.resume(throwing: error)
184202

185-
case .none:
186-
break
187-
}
203+
case .none:
204+
break
188205
}
189206
}
190207

191208
if let element2 = try await base2Iterator.next() {
192-
self.stateMachine.withCriticalRegion { stateMachine in
193-
let action = stateMachine.elementProduced((nil, element2, nil))
209+
let action = self.stateMachine.withCriticalRegion { stateMachine in
210+
stateMachine.elementProduced((nil, element2, nil))
211+
}
194212

195-
switch action {
196-
case .resumeContinuation(let downstreamContinuation, let result):
197-
downstreamContinuation.resume(returning: result)
213+
switch action {
214+
case .resumeContinuation(let downstreamContinuation, let result):
215+
downstreamContinuation.resume(returning: result)
198216

199-
case .none:
200-
break
201-
}
217+
case .none:
218+
break
202219
}
203220
} else {
204-
self.stateMachine.withCriticalRegion { stateMachine in
205-
let action = stateMachine.upstreamFinished()
221+
let action = self.stateMachine.withCriticalRegion { stateMachine in
222+
stateMachine.upstreamFinished()
223+
}
206224

207-
switch action {
208-
case .resumeContinuationWithNilAndCancelTaskAndUpstreamContinuations(
209-
let downstreamContinuation,
210-
let task,
211-
let upstreamContinuations
212-
):
225+
switch action {
226+
case .resumeContinuationWithNilAndCancelTaskAndUpstreamContinuations(
227+
let downstreamContinuation,
228+
let task,
229+
let upstreamContinuations
230+
):
213231

214-
upstreamContinuations.forEach { $0.resume(throwing: CancellationError()) }
215-
task.cancel()
232+
upstreamContinuations.forEach { $0.resume(throwing: CancellationError()) }
233+
task.cancel()
216234

217-
downstreamContinuation.resume(returning: .success(nil))
235+
downstreamContinuation.resume(returning: .success(nil))
218236

219-
case .none:
220-
break
221-
}
237+
case .none:
238+
break
222239
}
223240
}
224241
}
@@ -233,53 +250,53 @@ final class ZipStorage<Base1: AsyncSequence, Base2: AsyncSequence, Base3: AsyncS
233250
// element from upstream. This continuation is only resumed
234251
// if the downstream consumer called `next` to signal his demand.
235252
try await withUnsafeThrowingContinuation { continuation in
236-
self.stateMachine.withCriticalRegion { stateMachine in
237-
let action = stateMachine.childTaskSuspended(baseIndex: 2, continuation: continuation)
253+
let action = self.stateMachine.withCriticalRegion { stateMachine in
254+
stateMachine.childTaskSuspended(baseIndex: 2, continuation: continuation)
255+
}
238256

239-
switch action {
240-
case .resumeContinuation(let upstreamContinuation):
241-
upstreamContinuation.resume()
257+
switch action {
258+
case .resumeContinuation(let upstreamContinuation):
259+
upstreamContinuation.resume()
242260

243-
case .resumeContinuationWithError(let upstreamContinuation, let error):
244-
upstreamContinuation.resume(throwing: error)
261+
case .resumeContinuationWithError(let upstreamContinuation, let error):
262+
upstreamContinuation.resume(throwing: error)
245263

246-
case .none:
247-
break
248-
}
264+
case .none:
265+
break
249266
}
250267
}
251268

252269
if let element3 = try await base3Iterator.next() {
253-
self.stateMachine.withCriticalRegion { stateMachine in
254-
let action = stateMachine.elementProduced((nil, nil, element3))
270+
let action = self.stateMachine.withCriticalRegion { stateMachine in
271+
stateMachine.elementProduced((nil, nil, element3))
272+
}
255273

256-
switch action {
257-
case .resumeContinuation(let downstreamContinuation, let result):
258-
downstreamContinuation.resume(returning: result)
274+
switch action {
275+
case .resumeContinuation(let downstreamContinuation, let result):
276+
downstreamContinuation.resume(returning: result)
259277

260-
case .none:
261-
break
262-
}
278+
case .none:
279+
break
263280
}
264281
} else {
265-
self.stateMachine.withCriticalRegion { stateMachine in
266-
let action = stateMachine.upstreamFinished()
282+
let action = self.stateMachine.withCriticalRegion { stateMachine in
283+
stateMachine.upstreamFinished()
284+
}
267285

268-
switch action {
269-
case .resumeContinuationWithNilAndCancelTaskAndUpstreamContinuations(
270-
let downstreamContinuation,
271-
let task,
272-
let upstreamContinuations
273-
):
286+
switch action {
287+
case .resumeContinuationWithNilAndCancelTaskAndUpstreamContinuations(
288+
let downstreamContinuation,
289+
let task,
290+
let upstreamContinuations
291+
):
274292

275-
upstreamContinuations.forEach { $0.resume(throwing: CancellationError()) }
276-
task.cancel()
293+
upstreamContinuations.forEach { $0.resume(throwing: CancellationError()) }
294+
task.cancel()
277295

278-
downstreamContinuation.resume(returning: .success(nil))
296+
downstreamContinuation.resume(returning: .success(nil))
279297

280-
case .none:
281-
break
282-
}
298+
case .none:
299+
break
283300
}
284301
}
285302
}
@@ -291,25 +308,25 @@ final class ZipStorage<Base1: AsyncSequence, Base2: AsyncSequence, Base3: AsyncS
291308
try await group.next()
292309
} catch {
293310
// One of the upstream sequences threw an error
294-
self.stateMachine.withCriticalRegion { stateMachine in
295-
let action = stateMachine.upstreamThrew(error)
296-
switch action {
297-
case .resumeContinuationWithErrorAndCancelTaskAndUpstreamContinuations(
298-
let downstreamContinuation,
299-
let error,
300-
let task,
301-
let upstreamContinuations
302-
):
303-
upstreamContinuations.forEach { $0.resume(throwing: CancellationError()) }
304-
task.cancel()
305-
306-
downstreamContinuation.resume(returning: .failure(error))
307-
case .none:
308-
break
309-
}
310-
}
311+
let action = self.stateMachine.withCriticalRegion { stateMachine in
312+
stateMachine.upstreamThrew(error)
313+
}
314+
switch action {
315+
case .resumeContinuationWithErrorAndCancelTaskAndUpstreamContinuations(
316+
let downstreamContinuation,
317+
let error,
318+
let task,
319+
let upstreamContinuations
320+
):
321+
upstreamContinuations.forEach { $0.resume(throwing: CancellationError()) }
322+
task.cancel()
323+
324+
downstreamContinuation.resume(returning: .failure(error))
325+
case .none:
326+
break
327+
}
311328

312-
group.cancelAll()
329+
group.cancelAll()
313330
}
314331
}
315332
}

0 commit comments

Comments
 (0)
Please sign in to comment.