Skip to content

Commit 514c714

Browse files
authored
Improve the debounce performance (#105)
1 parent 9916d18 commit 514c714

File tree

4 files changed

+93
-17
lines changed

4 files changed

+93
-17
lines changed

Sources/OneWay/AnyEffect.swift

Lines changed: 64 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,31 @@ public struct AnyEffect<Element>: Effect where Element: Sendable {
4646

4747
/// Sends elements only after a specified time interval elapses between events.
4848
///
49+
/// First, create a Hashable ID that will be used to identify the debounce effect:
50+
///
51+
/// ```swift
52+
/// enum DebounceID {
53+
/// case searchText
54+
/// }
55+
/// ```
56+
///
57+
/// Then, apply the `debounce` modifier using the defined ID:
58+
///
59+
/// ```swift
60+
/// func reduce(state: inout State, action: Action) -> AnyEffect<Action> {
61+
/// switch action {
62+
/// // ...
63+
/// case let .search(text):
64+
/// return .single {
65+
/// let result = await api.request(text)
66+
/// return .setResult(result)
67+
/// }
68+
/// .debounce(id: DebounceID.searchText, for: 0.5)
69+
/// // ...
70+
/// }
71+
/// }
72+
/// ```
73+
///
4974
/// - Parameters:
5075
/// - id: The effect's identifier.
5176
/// - seconds: The duration for which the effect should wait before sending an element.
@@ -54,16 +79,20 @@ public struct AnyEffect<Element>: Effect where Element: Sendable {
5479
id: some EffectID,
5580
for seconds: Double
5681
) -> Self {
82+
let base = base
5783
var copy = self
5884
copy.method = .register(id, cancelInFlight: true)
59-
let values = copy.values
6085
copy.base = Effects.Sequence(
6186
operation: { send in
6287
guard !Task.isCancelled else { return }
6388
let NSEC_PER_SEC: Double = 1_000_000_000
6489
let dueTime = NSEC_PER_SEC * seconds
65-
try? await Task.sleep(nanoseconds: UInt64(dueTime))
66-
for await value in values {
90+
do {
91+
try await Task.sleep(nanoseconds: UInt64(dueTime))
92+
} catch {
93+
return
94+
}
95+
for await value in base.values {
6796
guard !Task.isCancelled else { return }
6897
send(value)
6998
}
@@ -74,6 +103,31 @@ public struct AnyEffect<Element>: Effect where Element: Sendable {
74103

75104
/// Sends elements only after a specified time interval elapses between events.
76105
///
106+
/// First, create a Hashable ID that will be used to identify the debounce effect:
107+
///
108+
/// ```swift
109+
/// enum DebounceID {
110+
/// case searchText
111+
/// }
112+
/// ```
113+
///
114+
/// Then, apply the `debounce` modifier using the defined ID:
115+
///
116+
/// ```swift
117+
/// func reduce(state: inout State, action: Action) -> AnyEffect<Action> {
118+
/// switch action {
119+
/// // ...
120+
/// case let .search(text):
121+
/// return .single {
122+
/// let result = await api.request(text)
123+
/// return .setResult(result)
124+
/// }
125+
/// .debounce(id: DebounceID.searchText, for: .milliseconds(500))
126+
/// // ...
127+
/// }
128+
/// }
129+
/// ```
130+
///
77131
/// - Parameters:
78132
/// - id: The effect's identifier.
79133
/// - dueTime: The duration for which the effect should wait before sending an element.
@@ -85,14 +139,18 @@ public struct AnyEffect<Element>: Effect where Element: Sendable {
85139
for dueTime: C.Instant.Duration,
86140
clock: C = ContinuousClock()
87141
) -> Self {
142+
let base = base
88143
var copy = self
89144
copy.method = .register(id, cancelInFlight: true)
90-
let values = copy.values
91145
copy.base = Effects.Sequence(
92146
operation: { send in
93147
guard !Task.isCancelled else { return }
94-
try? await clock.sleep(for: dueTime)
95-
for await value in values {
148+
do {
149+
try await clock.sleep(for: dueTime)
150+
} catch {
151+
return
152+
}
153+
for await value in base.values {
96154
guard !Task.isCancelled else { return }
97155
send(value)
98156
}

Sources/OneWay/Effect.swift

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -80,11 +80,14 @@ public enum Effects {
8080

8181
public var values: AsyncStream<Element> {
8282
AsyncStream { continuation in
83-
Task(priority: priority) {
83+
let task = Task(priority: priority) {
8484
let result = await operation()
8585
continuation.yield(result)
8686
continuation.finish()
8787
}
88+
continuation.onTermination = { _ in
89+
task.cancel()
90+
}
8891
}
8992
}
9093
}
@@ -111,10 +114,13 @@ public enum Effects {
111114

112115
public var values: AsyncStream<Element> {
113116
AsyncStream { continuation in
114-
Task(priority: priority) {
117+
let task = Task(priority: priority) {
115118
await operation { continuation.yield($0) }
116119
continuation.finish()
117120
}
121+
continuation.onTermination = { _ in
122+
task.cancel()
123+
}
118124
}
119125
}
120126
}
@@ -141,14 +147,18 @@ public enum Effects {
141147

142148
public var values: AsyncStream<Element> {
143149
AsyncStream { continuation in
144-
Task(priority: priority) {
150+
let task = Task(priority: priority) {
145151
for effect in effects {
146152
for await value in effect.values {
153+
guard !Task.isCancelled else { break }
147154
continuation.yield(value)
148155
}
149156
}
150157
continuation.finish()
151158
}
159+
continuation.onTermination = { _ in
160+
task.cancel()
161+
}
152162
}
153163
}
154164
}
@@ -175,12 +185,13 @@ public enum Effects {
175185

176186
public var values: AsyncStream<Element> {
177187
AsyncStream { continuation in
178-
Task(priority: priority) {
188+
let task = Task(priority: priority) {
179189
if #available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) {
180190
await withDiscardingTaskGroup { group in
181191
for effect in effects {
182192
group.addTask {
183193
for await value in effect.values {
194+
guard !Task.isCancelled else { break }
184195
continuation.yield(value)
185196
}
186197
}
@@ -192,6 +203,7 @@ public enum Effects {
192203
for effect in effects {
193204
group.addTask {
194205
for await value in effect.values {
206+
guard !Task.isCancelled else { break }
195207
continuation.yield(value)
196208
}
197209
}
@@ -200,6 +212,9 @@ public enum Effects {
200212
continuation.finish()
201213
}
202214
}
215+
continuation.onTermination = { _ in
216+
task.cancel()
217+
}
203218
}
204219
}
205220
}

Sources/OneWay/Store.swift

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -85,12 +85,11 @@ where R.Action: Sendable, R.State: Sendable & Equatable {
8585
guard !isProcessing else { return }
8686
isProcessing = true
8787
await Task.yield()
88-
let count = actionQueue.count
89-
for index in Int.zero ..< count {
90-
let action = actionQueue[index]
88+
for action in actionQueue {
9189
let taskID = TaskID()
9290
let effect = reducer.reduce(state: &state, action: action)
9391
let task = Task { [weak self, taskID] in
92+
guard !Task.isCancelled else { return }
9493
for await value in effect.values {
9594
guard let self else { break }
9695
guard !Task.isCancelled else { break }
@@ -102,14 +101,18 @@ where R.Action: Sendable, R.State: Sendable & Equatable {
102101

103102
switch effect.method {
104103
case let .register(id, cancelInFlight):
104+
let effectID = EffectIDWrapper(id)
105105
if cancelInFlight {
106-
let taskIDs = cancellables[EffectIDWrapper(id), default: []]
106+
let taskIDs = cancellables[effectID, default: []]
107107
taskIDs.forEach { removeTask($0) }
108+
cancellables.removeValue(forKey: effectID)
108109
}
109-
cancellables[EffectIDWrapper(id), default: []].insert(taskID)
110+
cancellables[effectID, default: []].insert(taskID)
110111
case let .cancel(id):
111-
let taskIDs = cancellables[EffectIDWrapper(id), default: []]
112+
let effectID = EffectIDWrapper(id)
113+
let taskIDs = cancellables[effectID, default: []]
112114
taskIDs.forEach { removeTask($0) }
115+
cancellables.removeValue(forKey: effectID)
113116
case .none:
114117
break
115118
}

Tests/OneWayTests/StoreTests.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -323,7 +323,7 @@ private struct TestReducer: Reducer {
323323

324324
case .longTimeTask:
325325
return .single {
326-
try! await clock.sleep(for: .seconds(200))
326+
try? await clock.sleep(for: .seconds(200))
327327
return Action.response("Success")
328328
}
329329
.cancellable(EffectID.longTimeTask)

0 commit comments

Comments
 (0)