import Foundation enum WatchSessionActivationError: LocalizedError { case failed(String) case timedOut var errorDescription: String? { switch self { case let .failed(reason): "WATCH_UNAVAILABLE: Apple Watch session activation failed (\(reason))" case .timedOut: "WATCH_UNAVAILABLE: Apple Watch session activation timed out" } } } /// Joins concurrent sends to one WCSession activation and bounds the wait for its delegate callback. /// A failed or timed-out generation remains retryable so a later foreground launch can recover. final class WatchSessionActivationGate: @unchecked Sendable { private typealias Waiter = CheckedContinuation private enum State { case idle case activating(UInt64) case completed(Result) } private let lock = NSLock() private let timeoutNanoseconds: UInt64 private var generation: UInt64 = 0 private var state = State.idle private var waiters: [Waiter] = [] init(timeoutNanoseconds: UInt64 = 15_000_000_000) { self.timeoutNanoseconds = timeoutNanoseconds } @discardableResult func beginActivation() -> Bool { let generationToStart: UInt64? = self.lock.withLock { switch self.state { case .idle: break case .activating: return nil case let .completed(result): if case .success = result { return nil } } self.generation &+= 1 self.state = .activating(self.generation) return self.generation } guard let generationToStart else { return false } Task { [weak self] in do { try await Task.sleep(nanoseconds: self?.timeoutNanoseconds ?? 0) } catch { return } self?.finish(.failure(.timedOut), generation: generationToStart) } return true } func waitUntilActivated() async throws { try await withCheckedThrowingContinuation { (continuation: Waiter) in let completedResult: Result? = self.lock.withLock { switch self.state { case .idle: return .failure(.failed("activation was not started")) case .activating: self.waiters.append(continuation) return nil case let .completed(result): return result } } if let completedResult { Self.resume(continuation, with: completedResult) } } } func complete(activated: Bool, errorDescription: String?) { let result: Result if activated { result = .success(()) } else { let reason = errorDescription?.trimmingCharacters(in: .whitespacesAndNewlines) let failureReason = reason.flatMap { $0.isEmpty ? nil : $0 } ?? "session stayed inactive" result = .failure(.failed(failureReason)) } self.finish(result, generation: nil) } func reset() { let waiters: [Waiter] = self.lock.withLock { self.state = .idle let waiters = self.waiters self.waiters.removeAll() return waiters } let result = Result.failure( .failed("active Apple Watch changed")) for waiter in waiters { Self.resume(waiter, with: result) } } private func finish( _ result: Result, generation expectedGeneration: UInt64?) { let waiters: [Waiter]? = self.lock.withLock { if let expectedGeneration { guard case let .activating(activeGeneration) = self.state, activeGeneration == expectedGeneration else { return nil } } self.state = .completed(result) let waiters = self.waiters self.waiters.removeAll() return waiters } guard let waiters else { return } for waiter in waiters { Self.resume(waiter, with: result) } } private static func resume( _ continuation: Waiter, with result: Result) { switch result { case .success: continuation.resume(returning: ()) case let .failure(error): continuation.resume(throwing: error) } } }