Skip to content

Commit ba3457d

Browse files
committed
Add .sticky dispatching option
1 parent f3b3a44 commit ba3457d

4 files changed

Lines changed: 108 additions & 19 deletions

File tree

Sources/DispatchState.swift

Lines changed: 30 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,9 @@ struct DispatchState: Dispatcher {
125125
// Does not change chain dispatcher, but does confirm if appropriate
126126
state.location.confirm()
127127
}
128+
case .sticky:
129+
state.chainStrategy = StickyStrategy()
130+
state.location = .primed
128131
}
129132
} else {
130133
state.chainStrategy = StandardStrategy(dispatcher: state.dispatcher)
@@ -190,7 +193,11 @@ struct DispatchState: Dispatcher {
190193
case .default:
191194
return (dispatcher: defaultDispatcher, explicit: true)
192195
case .chain:
196+
// ChainDispatchStrategy should have claimed this
193197
fatalError("`on: .chain` used without a chain dispatcher")
198+
case .sticky:
199+
// Repeat previous dispatcher
200+
return (dispatcher: self.dispatcher, explicit: true)
194201
}
195202
}
196203

@@ -218,13 +225,16 @@ struct StandardStrategy: ChainDispatchStrategy {
218225
guard let sentinel = given as? SentinelDispatcher else { return nil }
219226

220227
switch sentinel.type {
221-
case .default:
222-
// Explicit; punt
223-
return nil
224-
case .unspecified:
225-
return (dispatcher: dispatcher, explicit: false)
226-
case .chain:
227-
return (dispatcher: dispatcher, explicit: true)
228+
case .default:
229+
// Explicit; punt
230+
return nil
231+
case .unspecified:
232+
return (dispatcher: dispatcher, explicit: false)
233+
case .chain:
234+
return (dispatcher: dispatcher, explicit: true)
235+
case .sticky:
236+
return nil
237+
228238
}
229239
}
230240
}
@@ -234,17 +244,19 @@ struct StickyStrategy: ChainDispatchStrategy {
234244

235245
func nextDispatcher(previous: DispatchState, givenDispatcher given: Dispatcher) -> SourcedDispatcher? {
236246

237-
// Chain dispatcher has nothing to say about explicitly specified dispatchers
238-
guard let sentinel = given as? SentinelDispatcher else { return nil }
239-
240-
switch sentinel.type {
241-
case .default:
242-
// Explicit; punt
243-
return nil
244-
case .unspecified:
245-
return (dispatcher: previous.dispatcher, explicit: false)
246-
case .chain:
247-
return (dispatcher: previous.dispatcher, explicit: true)
247+
if let sentinel = given as? SentinelDispatcher {
248+
switch sentinel.type {
249+
case .default:
250+
// Explicit; punt
251+
return nil
252+
case .unspecified:
253+
return (dispatcher: previous.dispatcher, explicit: false)
254+
case .chain, .sticky:
255+
return (dispatcher: previous.dispatcher, explicit: true)
256+
}
257+
} else {
258+
// Claim an explicit dispatcher because we do want to ratify the chain dispatcher in this case
259+
return (dispatcher: given, explicit: true)
248260
}
249261
}
250262
}

Sources/Dispatcher.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ public extension DispatchQueue {
7373
static let unspecified = DispatchQueue(label: "unspecified.promisekit.org") // Parameter not provided
7474
static let `default` = DispatchQueue(label: "default.promisekit.org") // Explicit request for default behavior
7575
static let chain = DispatchQueue(label: "chain.promisekit.org") // Execute on same Dispatcher as previous closure
76+
static let sticky = DispatchQueue(label: "sticky.promisekit.org") // Reuse the previous dispatcher if not explicitly specified
7677
}
7778

7879
public extension DispatchQueue {
@@ -110,7 +111,8 @@ extension DispatchQueue {
110111
case .unspecified: return SentinelDispatcher(type: .unspecified, flags: flags)
111112
case .default: return SentinelDispatcher(type: .default, flags: flags)
112113
case .chain: return SentinelDispatcher(type: .chain, flags: flags)
113-
default:
114+
case .sticky: return SentinelDispatcher(type: .sticky, flags: flags)
115+
default:
114116
if let flags = flags {
115117
return self.asDispatcher(flags: flags)
116118
}

Sources/Dispatchers/SentinelDispatcher.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ struct SentinelDispatcher: Dispatcher {
88
case unspecified // Parameter not provided
99
case `default` // Explicit request for global default
1010
case chain // Explicit request to use the chain dispatcher
11+
case sticky // Continue using same dispatcher until changed
1112
}
1213

1314
let type: SentinelType

Tests/Core/ChainDispatcherTests.swift

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,5 +237,79 @@ class ChainDispatcherTests: XCTestCase {
237237
XCTAssert(log == "failedToConfirmChainDispatcher")
238238
}
239239

240+
func testStickyChainDispatcher() {
241+
let ex = expectation(description: "Sticky chain dispatcher")
242+
let dispatcher = RecordingDispatcher()
243+
let log = captureLog {
244+
Promise.value(42).dispatch(on: .sticky).then {
245+
Promise.value($0 + 10)
246+
}.map {
247+
$0 + 20
248+
}.done(on: dispatcher) {
249+
XCTAssert($0 == 72)
250+
}.map {
251+
123 // Tail
252+
}.catch { error in
253+
// NOP - not dispatched
254+
}.finally {
255+
ex.fulfill()
256+
}
257+
}
258+
waitForExpectations(timeout: 1)
259+
XCTAssert(defaultBodyDispatcher.dispatchCount == 2)
260+
XCTAssert(defaultTailDispatcher.dispatchCount == 0)
261+
XCTAssert(dispatcher.dispatchCount == 3)
262+
XCTAssert(log == nil)
263+
}
264+
265+
func testUnconfirmedStickyChainDispatcher() {
266+
let ex = expectation(description: "Unconfirmed sticky chain dispatcher")
267+
let dispatcher = RecordingDispatcher()
268+
let log = captureLog {
269+
Promise.value(42).dispatch(on: .sticky).then(on: dispatcher) {
270+
Promise.value($0 + 10)
271+
}.map {
272+
$0 + 20
273+
}.done {
274+
XCTAssert($0 == 72)
275+
}.map(on: defaultTailDispatcher) {
276+
123 // Tail
277+
}.catch { error in
278+
// NOP - not dispatched
279+
}.finally {
280+
ex.fulfill()
281+
}
282+
}
283+
waitForExpectations(timeout: 1)
284+
XCTAssert(defaultBodyDispatcher.dispatchCount == 0)
285+
XCTAssert(defaultTailDispatcher.dispatchCount == 2)
286+
XCTAssert(dispatcher.dispatchCount == 3)
287+
XCTAssert(log == "failedToConfirmChainDispatcher")
288+
}
289+
290+
func testAdHocStickyDispatchers() {
291+
let ex = expectation(description: "Ad hoc sticky dispatching")
292+
let dispatcher = RecordingDispatcher()
293+
let log = captureLog {
294+
Promise.value(42).then(on: dispatcher) {
295+
Promise.value($0 + 10)
296+
}.map(on: .sticky) {
297+
$0 + 20
298+
}.done(on: .sticky) {
299+
XCTAssert($0 == 72)
300+
}.map(on: defaultBodyDispatcher) {
301+
123 // Tail
302+
}.catch(on: .sticky) { error in
303+
// NOP - not dispatched
304+
}.finally {
305+
ex.fulfill()
306+
}
307+
}
308+
waitForExpectations(timeout: 1)
309+
XCTAssert(defaultBodyDispatcher.dispatchCount == 1)
310+
XCTAssert(defaultTailDispatcher.dispatchCount == 1)
311+
XCTAssert(dispatcher.dispatchCount == 3)
312+
XCTAssert(log == nil)
313+
}
240314
}
241315

0 commit comments

Comments
 (0)