Remove ChainedPromise

This commit is contained in:
Max Radermacher 2026-05-20 01:25:57 -05:00 committed by GitHub
parent 1105ac39a3
commit 7a30fc750e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 0 additions and 305 deletions

View File

@ -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 */,

View File

@ -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
}
}

View File

@ -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)
}
}
}