Remove ChainedPromise
This commit is contained in:
parent
1105ac39a3
commit
7a30fc750e
@ -926,8 +926,6 @@
|
||||
6612780D2996BD0300A1D5A1 /* RegistrationCoordinatorTestShims.swift in Sources */ = {isa = PBXBuildFile; fileRef = 661278092996BAB400A1D5A1 /* RegistrationCoordinatorTestShims.swift */; };
|
||||
66138FB6298326C7002E0CFE /* SecureValueRecovery.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66138FB5298326C7002E0CFE /* SecureValueRecovery.swift */; };
|
||||
661396AB28BD53EF00E0C4DF /* HiddenStoryHeaderCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 661396AA28BD53EF00E0C4DF /* HiddenStoryHeaderCell.swift */; };
|
||||
661396AD28BE74DC00E0C4DF /* ChainedPromise.swift in Sources */ = {isa = PBXBuildFile; fileRef = 661396AC28BE74DC00E0C4DF /* ChainedPromise.swift */; };
|
||||
661396AF28BE881E00E0C4DF /* ChainedPromiseTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 661396AE28BE881E00E0C4DF /* ChainedPromiseTest.swift */; };
|
||||
661429192D35B9F10043AA22 /* BackupArchive+Timestamp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 661429182D35B9EA0043AA22 /* BackupArchive+Timestamp.swift */; };
|
||||
66144B2F2BF7FB5200E2C9CD /* WallpaperImageStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66144B2E2BF7FB5200E2C9CD /* WallpaperImageStore.swift */; };
|
||||
66144B312BF7FB7B00E2C9CD /* WallpaperImageStoreImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66144B302BF7FB7B00E2C9CD /* WallpaperImageStoreImpl.swift */; };
|
||||
@ -5194,8 +5192,6 @@
|
||||
661278092996BAB400A1D5A1 /* RegistrationCoordinatorTestShims.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RegistrationCoordinatorTestShims.swift; sourceTree = "<group>"; };
|
||||
66138FB5298326C7002E0CFE /* SecureValueRecovery.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureValueRecovery.swift; sourceTree = "<group>"; };
|
||||
661396AA28BD53EF00E0C4DF /* HiddenStoryHeaderCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HiddenStoryHeaderCell.swift; sourceTree = "<group>"; };
|
||||
661396AC28BE74DC00E0C4DF /* ChainedPromise.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChainedPromise.swift; sourceTree = "<group>"; };
|
||||
661396AE28BE881E00E0C4DF /* ChainedPromiseTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChainedPromiseTest.swift; sourceTree = "<group>"; };
|
||||
661429182D35B9EA0043AA22 /* BackupArchive+Timestamp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BackupArchive+Timestamp.swift"; sourceTree = "<group>"; };
|
||||
66144B2E2BF7FB5200E2C9CD /* WallpaperImageStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WallpaperImageStore.swift; sourceTree = "<group>"; };
|
||||
66144B302BF7FB7B00E2C9CD /* WallpaperImageStoreImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WallpaperImageStoreImpl.swift; sourceTree = "<group>"; };
|
||||
@ -10461,7 +10457,6 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
668A01172C2B6088007B8808 /* Catchable.swift */,
|
||||
661396AC28BE74DC00E0C4DF /* ChainedPromise.swift */,
|
||||
668A01182C2B6088007B8808 /* DispatchQueue+Promise.swift */,
|
||||
668A01192C2B6088007B8808 /* firstly.swift */,
|
||||
668A011A2C2B6088007B8808 /* Future.swift */,
|
||||
@ -14323,7 +14318,6 @@
|
||||
F908C67A29F08E4E00C3EFC4 /* AppExpiryTest.swift */,
|
||||
66B1E26F2CB48C48005F43AC /* Array+SSKTest.swift */,
|
||||
F9C612B3284E466B00B2199A /* CGPointExtensionsTest.swift */,
|
||||
661396AE28BE881E00E0C4DF /* ChainedPromiseTest.swift */,
|
||||
F962B38B293F9F9F00765BD8 /* CRC32Test.swift */,
|
||||
509BBF7928CA556700F4D8A0 /* Data+SSKTest.swift */,
|
||||
724E68632C91FA73002199F3 /* DataHexadecimalTest.swift */,
|
||||
@ -19142,7 +19136,6 @@
|
||||
F9C5CC0C289453B300548EEE /* CDNDownloadOperation.swift in Sources */,
|
||||
727328072CA6CF570080E2C7 /* Certificates.swift in Sources */,
|
||||
508F05A52FA28308004B96E5 /* CGDataProvider+SSK.swift in Sources */,
|
||||
661396AD28BE74DC00E0C4DF /* ChainedPromise.swift in Sources */,
|
||||
50E5E4B32993352C00E15A1C /* ChangePhoneNumberPniManager.swift in Sources */,
|
||||
6603AC2D29C220F30079BC82 /* ChangePhoneNumberPniManagerMock.swift in Sources */,
|
||||
66F6D6A32C7D0CCA00EFAF75 /* ChatColors.swift in Sources */,
|
||||
@ -20130,7 +20123,6 @@
|
||||
500AF3AD2C5834A400CB9F4F /* CancellableContinuationTest.swift in Sources */,
|
||||
508622AD2D026F5200931BF9 /* CanonicalPhoneNumberTest.swift in Sources */,
|
||||
725464F62B9FAC7600EABFD2 /* CGPointExtensionsTest.swift in Sources */,
|
||||
661396AF28BE881E00E0C4DF /* ChainedPromiseTest.swift in Sources */,
|
||||
D985D86829B94EC60087C90C /* ChangePhoneNumberPniManagerTest.swift in Sources */,
|
||||
F9C9610B29A91026001E4A09 /* ChatServiceAuthTest.swift in Sources */,
|
||||
507529172F6494C2000F72F0 /* CipherContextTest.swift in Sources */,
|
||||
|
||||
@ -1,167 +0,0 @@
|
||||
//
|
||||
// Copyright 2022 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// Wraps a promise to execute promises sequentially in a thread-safe way.
|
||||
///
|
||||
/// When a closure which returns a promise is enqueued on the `ChainedPromise`,
|
||||
/// the closure itself will not be executed until all previously enqueued promises have
|
||||
/// been resolved. (Note that failures are ignored)
|
||||
/// In other words, you "enqueue" blocks of work whose results are represented by
|
||||
/// promises, and guarantee that they are executed serially with FIFO.
|
||||
///
|
||||
/// Enqueue calls are thread-safe.
|
||||
///
|
||||
/// WARNING: for this reason, if an enqueued closure returns a promise that never resolves,
|
||||
/// the entire chain will be left waiting forever.
|
||||
///
|
||||
/// For example, consider the following sequence of calls:
|
||||
/// ```
|
||||
/// let chainedPromise = ChainedPromise<Void>()
|
||||
/// let createFilePromise = chainedPromise.enqueue {
|
||||
/// self.createFileOnDisk()
|
||||
/// }
|
||||
/// let validateFilePromise = chainedPromise.enqueue {
|
||||
/// self.validateFileOnDisk()
|
||||
/// }
|
||||
/// ```
|
||||
/// In this example, the call to `validateFileOnDisk` will not be executed until
|
||||
/// the promise returned by `createFileOnDisk` is resolved (whether success or failure).
|
||||
/// Anything enqueued afterwards will wait on the resolution of the promise returned by
|
||||
/// `validateFileOnDisk`.
|
||||
///
|
||||
public class ChainedPromise<Value> {
|
||||
|
||||
private let scheduler: Scheduler
|
||||
private var currentPromise: Promise<Value>
|
||||
|
||||
/// Create a new ChainedPromise.
|
||||
///
|
||||
/// Each ChainedPromise is independent; you typically create a single instance and enqueue multiple
|
||||
/// blocks of work on it.
|
||||
///
|
||||
/// - Parameter initialValue: the value that will be used as the "previous value" input given to the first enqueued block.
|
||||
/// - Parameter scheduler: The scheduler to use to serialize all calls. Defaults to a new unique serial background queue.
|
||||
public init(initialValue: Value, scheduler: Scheduler = DispatchQueue(label: UUID().uuidString)) {
|
||||
self.scheduler = scheduler
|
||||
self.currentPromise = .value(initialValue)
|
||||
}
|
||||
|
||||
// MARK: Primary Enqueuing
|
||||
|
||||
/// Enqueue a block of work to be executed when all previous enqueued work has completed.
|
||||
/// Future enqueued blocks will not begin until the returned promise is resolved.
|
||||
///
|
||||
/// - Parameter recoverValue: The value to fallback to if the Promise returned by `nextPromise` fails.
|
||||
/// This the value that will be given as input to the next enqueued block.
|
||||
/// - Parameter nextPromise: A closure to be executed when previous enqueued work has
|
||||
/// completed, returning a promise whose resolution blocks future enqueued work. Takes the previous result as input.
|
||||
/// - Parameter map: Maps from the type of the `nextPromise` return type to the type of the root ChainedPromise.
|
||||
/// - Returns a promise representing the result of the provided block, when it eventually executes.
|
||||
public func enqueue<T>(
|
||||
recoverValue: T,
|
||||
_ nextPromise: @escaping (Value) -> Promise<T>,
|
||||
_ map: @escaping (T) -> Value,
|
||||
) -> Promise<T> {
|
||||
_enqueue(nextPromise, recoverValue: map(recoverValue), map: map)
|
||||
}
|
||||
|
||||
// MARK: Convenience Enqueueing methods
|
||||
|
||||
/// Enqueue a block of work to be executed when all previous enqueued work has completed.
|
||||
/// Future enqueued blocks will not begin until the returned promise is resolved.
|
||||
///
|
||||
/// - Parameter recoverValue: The value to fallback to if the Promise returned by `nextPromise` fails.
|
||||
/// This the value that will be given as input to the next enqueued block.
|
||||
/// - Parameter nextPromise: A closure to be executed when previous enqueued work has
|
||||
/// completed, returning a promise whose resolution blocks future enqueued work. Takes the previous result as input.
|
||||
/// - Returns a promise representing the result of the provided block, when it eventually executes.
|
||||
public func enqueue(
|
||||
recoverValue: Value,
|
||||
_ nextPromise: @escaping (Value) -> Promise<Value>,
|
||||
) -> Promise<Value> {
|
||||
_enqueue(nextPromise, recoverValue: recoverValue)
|
||||
}
|
||||
}
|
||||
|
||||
extension ChainedPromise where Value == Void {
|
||||
|
||||
/// Create a new ChainedPromise.
|
||||
///
|
||||
/// Each ChainedPromise is independent; you typically create a single instance and enqueue multiple
|
||||
/// blocks of work on it.
|
||||
///
|
||||
/// - Parameter scheduler: The scheduler to use to serialize all calls. Defaults to a new unique serial background queue.
|
||||
convenience init(scheduler: Scheduler = DispatchQueue(label: UUID().uuidString)) {
|
||||
self.init(initialValue: (), scheduler: scheduler)
|
||||
}
|
||||
|
||||
/// Enqueue a block of work to be executed when all previous enqueued work has completed.
|
||||
/// Future enqueued blocks will not begin until the returned promise is resolved.
|
||||
///
|
||||
/// - Parameter nextPromise: A closure to be executed when previous enqueued work has
|
||||
/// completed, returning a promise whose resolution blocks future enqueued work.
|
||||
/// - Returns a promise representing the result of the provided block, when it eventually executes.
|
||||
public func enqueue(
|
||||
_ nextPromise: @escaping () -> Promise<Void>,
|
||||
) -> Promise<Void> {
|
||||
_enqueue(nextPromise, recoverValue: ())
|
||||
}
|
||||
|
||||
/// Enqueue a block of work to be executed when all previous enqueued work has completed.
|
||||
/// Future enqueued blocks will not begin until the returned promise is resolved.
|
||||
///
|
||||
/// - Parameter nextPromise: A closure to be executed when previous enqueued work has
|
||||
/// completed, returning a promise whose resolution blocks future enqueued work.
|
||||
/// - Returns a promise representing the result of the provided block, when it eventually executes.
|
||||
public func enqueue<T>(
|
||||
_ nextPromise: @escaping () -> Promise<T>,
|
||||
) -> Promise<T> {
|
||||
_enqueue(nextPromise, recoverValue: (), map: { _ in () })
|
||||
}
|
||||
}
|
||||
|
||||
extension ChainedPromise {
|
||||
|
||||
// MARK: - Root implementation(s)
|
||||
|
||||
// Note there are independent implementations for mapped and unmapped versions
|
||||
// so as to avoid excessive queue-hopping when we run maps.
|
||||
|
||||
private func _enqueue(
|
||||
_ nextPromise: @escaping (Value) -> Promise<Value>,
|
||||
recoverValue: Value,
|
||||
) -> Promise<Value> {
|
||||
let (returnPromise, returnFuture) = Promise<Value>.pending()
|
||||
scheduler.asyncIfNecessary {
|
||||
let newPromise = self.currentPromise.then(on: self.scheduler) { prevValue in
|
||||
return nextPromise(prevValue)
|
||||
}
|
||||
returnFuture.resolve(on: SyncScheduler(), with: newPromise)
|
||||
self.currentPromise = newPromise
|
||||
.recover(on: SyncScheduler()) { _ -> Promise<Value> in .value(recoverValue) }
|
||||
}
|
||||
return returnPromise
|
||||
}
|
||||
|
||||
private func _enqueue<T>(
|
||||
_ nextPromise: @escaping (Value) -> Promise<T>,
|
||||
recoverValue: Value,
|
||||
map: @escaping (T) -> Value,
|
||||
) -> Promise<T> {
|
||||
let (returnPromise, returnFuture) = Promise<T>.pending()
|
||||
scheduler.asyncIfNecessary {
|
||||
let newPromise = self.currentPromise.then(on: self.scheduler) { prevValue in
|
||||
return nextPromise(prevValue)
|
||||
}
|
||||
returnFuture.resolve(on: SyncScheduler(), with: newPromise)
|
||||
self.currentPromise = newPromise
|
||||
.map(on: SyncScheduler(), map)
|
||||
.recover(on: SyncScheduler()) { _ -> Promise<Value> in .value(recoverValue) }
|
||||
}
|
||||
return returnPromise
|
||||
}
|
||||
}
|
||||
@ -1,130 +0,0 @@
|
||||
//
|
||||
// Copyright 2022 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
import Foundation
|
||||
public import XCTest
|
||||
@testable import SignalServiceKit
|
||||
|
||||
public class ChainedPromiseTest: XCTestCase {
|
||||
func testChainSingleVoidPromise() throws {
|
||||
let chainedPromise = ChainedPromise<Void>()
|
||||
|
||||
let promise = chainedPromise.enqueue {
|
||||
return .value(())
|
||||
}
|
||||
expectSuccess(promise, description: "", timeout: 1)
|
||||
}
|
||||
|
||||
func testChainSingleValuePromise() throws {
|
||||
let chainedPromise = ChainedPromise<String>(initialValue: "")
|
||||
let promise = chainedPromise.enqueue(recoverValue: "fail") { _ in
|
||||
.value("hello")
|
||||
}
|
||||
let value = expectSuccess(promise, description: "", timeout: 1)
|
||||
XCTAssertEqual(value, "hello")
|
||||
}
|
||||
|
||||
func testChainingPromiseSuccess() throws {
|
||||
let chainedPromise = ChainedPromise<Void>()
|
||||
|
||||
let (firstPromise, firstFuture) = Promise<Void>.pending()
|
||||
_ = chainedPromise.enqueue {
|
||||
return firstPromise
|
||||
}
|
||||
|
||||
let failIfExecuteSecondPromise = AtomicBool(true, lock: .sharedGlobal)
|
||||
let (secondPromise, secondFuture) = Promise<Void>.pending()
|
||||
let secondResultPromise = chainedPromise.enqueue {
|
||||
XCTAssertFalse(failIfExecuteSecondPromise.get())
|
||||
return secondPromise
|
||||
}
|
||||
|
||||
failIfExecuteSecondPromise.set(false)
|
||||
firstFuture.resolve(())
|
||||
secondFuture.resolve(())
|
||||
|
||||
expectSuccess(secondResultPromise, description: "", timeout: 1)
|
||||
}
|
||||
|
||||
func testChainingPromiseFailure() throws {
|
||||
let chainedPromise = ChainedPromise<Void>()
|
||||
|
||||
let (firstPromise, firstFuture) = Promise<Void>.pending()
|
||||
let firstResultPromise = chainedPromise.enqueue {
|
||||
return firstPromise
|
||||
}
|
||||
|
||||
let failIfExecuteSecondPromise = AtomicBool(true, lock: .sharedGlobal)
|
||||
let (secondPromise, secondFuture) = Promise<Void>.pending()
|
||||
let secondResultPromise = chainedPromise.enqueue {
|
||||
XCTAssertFalse(failIfExecuteSecondPromise.get())
|
||||
return secondPromise
|
||||
}
|
||||
|
||||
failIfExecuteSecondPromise.set(false)
|
||||
firstFuture.resolve(with: Promise<Void>(error: NSError()))
|
||||
secondFuture.resolve(())
|
||||
|
||||
expectFailure(firstResultPromise, description: "first result failure", timeout: 0.1)
|
||||
expectSuccess(secondResultPromise, description: "", timeout: 1)
|
||||
}
|
||||
|
||||
func testChainingPromiseChainedValue() throws {
|
||||
let chainedPromise = ChainedPromise<Int>(initialValue: 1)
|
||||
|
||||
let (firstPromise, firstFuture) = Promise<Int>.pending()
|
||||
_ = chainedPromise.enqueue(recoverValue: -1) {
|
||||
XCTAssertEqual($0, 1)
|
||||
return firstPromise
|
||||
}
|
||||
|
||||
let (secondPromise, secondFuture) = Promise<Int>.pending()
|
||||
let secondResultPromise = chainedPromise.enqueue(recoverValue: -1) {
|
||||
XCTAssertEqual($0, 2)
|
||||
return secondPromise
|
||||
}
|
||||
|
||||
firstFuture.resolve(2)
|
||||
secondFuture.resolve(3)
|
||||
|
||||
XCTAssertEqual(expectSuccess(secondResultPromise, description: "", timeout: 1), 3)
|
||||
}
|
||||
|
||||
func testChainingPromiseLongChain() throws {
|
||||
let chainedPromise = ChainedPromise<Void>()
|
||||
|
||||
// Set an initial promise to gate the others
|
||||
let (firstPromise, firstFuture) = Promise<Void>.pending()
|
||||
let firstResultPromise = chainedPromise.enqueue {
|
||||
return firstPromise
|
||||
}
|
||||
|
||||
let hasResolvedUpTo = AtomicUInt(0, lock: .sharedGlobal)
|
||||
var pendingFutures = [Future<Void>]()
|
||||
var resultPromises = [Promise<Void>]()
|
||||
var enqueuePromises = [Promise<Void>]()
|
||||
(1...10).forEach { i in
|
||||
let (promise, future) = Promise<Void>.pending()
|
||||
let (enqueuePromise, enqueueFuture) = Promise<Void>.pending()
|
||||
resultPromises.append(chainedPromise.enqueue {
|
||||
XCTAssertEqual(hasResolvedUpTo.get(), UInt(i - 1))
|
||||
enqueueFuture.resolve(())
|
||||
return promise
|
||||
})
|
||||
enqueuePromises.append(enqueuePromise)
|
||||
pendingFutures.append(future)
|
||||
}
|
||||
|
||||
firstFuture.resolve(())
|
||||
expectSuccess(firstResultPromise, description: "initial", timeout: 1)
|
||||
|
||||
pendingFutures.enumerated().forEach { i, future in
|
||||
expectSuccess(enqueuePromises[i], description: "\(i)-th enqueue promise", timeout: 1)
|
||||
hasResolvedUpTo.set(UInt(i + 1))
|
||||
future.resolve(())
|
||||
expectSuccess(resultPromises[i], description: "\(i)-th promise", timeout: 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user