TM-SGNL-iOS/SignalServiceKit/Preconditions/Preconditions.swift
TeleMessage developers dde0620daf initial commit
2025-05-03 12:28:28 -07:00

103 lines
4.7 KiB
Swift

//
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
import Foundation
/// A single Precondition that must be satisfied.
public protocol Precondition {
typealias WaitResult = _PreconditionWaitResult
/// Waits until the Precondition has been satisfied.
///
/// When this method returns, the Precondition **must** be satisfied. Again,
/// whatever the Precondition is **must** be true. (Or, I suppose you could
/// return that the Task was canceled.)
///
/// Types that implement this method should generally do so in two phases.
///
/// (1) Check if the Preconditions is currently satisfied. If it is, return
/// `.satisfiedImmediately`.
///
/// (2) If it's not, suspend until it is, and then return
/// `.wasNotSatisfiedButIsNow`. This will likely require listening for a
/// notification or some other asynchronous callback.
func waitUntilSatisfied() async -> WaitResult
}
public enum _PreconditionWaitResult {
/// The Precondition was satisfied when `waitUntilSatisfied` was invoked.
case satisfiedImmediately
/// The Precondition wasn't satisfied when `waitUntilSatisfied` was invoked,
/// so `waitUntilSatisfied` had to wait until it was satisfied.
case wasNotSatisfiedButIsNow
/// The Task was canceled while waiting for the Precondition.
case canceled
}
public final class Preconditions {
private let preconditions: [Precondition]
public init(_ preconditions: [Precondition]) {
self.preconditions = preconditions
}
/// Waits until every Precondition is satisfied.
///
/// In synchronous code, the caller doesn't return until all of its callees
/// have returned. In structured concurrency, the caller doesn't return
/// until all of its callees have returned, but now they might be doing
/// things asynchronously. But they do need to finish; they can't subscribe
/// to "infinite" callbacks because the caller would never return.
///
/// In unstructured concurrency, each Precondition might notify the
/// Preconditions (note the plural) object whenever its state changes. There
/// would generally be a `runIfReady` method, and that method would perform
/// a series of synchronous `isReady` checks. If they all pass, the
/// operation would start running. If any of them return false, the
/// `runIfReady` method would stop and return early; in doing so, it would
/// assume that something else will invoke it again. That will usually be a
/// `stateChanged` callback corresponding to one of the `isReady` checks.
/// For example, if `runIfReady` has an `isRegistered` check, then the
/// containing object would also need a `registrationStatusChanged`
/// notification observer that calls `runIfReady`.
///
/// In structured concurrency, the paradigm is different: the caller
/// (`runIfReady`) doles out asynchronous execution to the callees, and the
/// callees (`isReady`) are expected to remain within these asynchronous
/// execution contexts. As a result, they only set up observers when asked
/// to, in much the same way that `isReady` only does work when asked to.
///
/// This method operates by checking whether each Precondition is satisfied.
/// If all of them are satisfied, then it returns control to its caller. If
/// one of them isn't satisfied, then it will suspend, waiting for it to be
/// satisfied. Once that Precondition is satisfied, this method STARTS OVER.
///
/// It STARTS OVER because one Precondition might become unsatisfied while
/// waiting for another Precondition to be satisfied. For example, imagine
/// you have `AppActive` and `WebSocketOpen` Preconditions; if the user
/// backgrounds the app while waiting for the web socket to open, then when
/// the web socket does open, you need to check if the app is still active.
///
/// - Throws: An error if the `Task` is canceled.
public func waitUntilSatisfied() async throws {
try Task.checkCancellation()
for precondition in preconditions {
switch await precondition.waitUntilSatisfied() {
case .canceled:
throw CancellationError()
case .satisfiedImmediately:
// It was ready immediately, so we assume that it's still satisfied and
// check the next Precondition.
break
case .wasNotSatisfiedButIsNow:
// It wasn't ready immediately, so previous Preconditions in the loop might
// have changed. Let's start over and check if they're still satisfied.
try await waitUntilSatisfied()
return
}
}
}
}