From ea99e9002fc516004c339a2767e4023933df75f7 Mon Sep 17 00:00:00 2001 From: Max Radermacher Date: Mon, 25 Sep 2023 12:49:33 -0500 Subject: [PATCH] Add async-await support to Promises --- SignalCoreKit/src/Promises/AnyPromise.swift | 4 ++ SignalCoreKit/src/Promises/Guarantee.swift | 28 +++++++++++ SignalCoreKit/src/Promises/Promise.swift | 27 +++++++++++ .../src/Promises/SyncScheduler.swift | 47 +++++++++++++++++++ SignalCoreKitTests/src/PromiseTests.swift | 12 +++++ 5 files changed, 118 insertions(+) create mode 100644 SignalCoreKit/src/Promises/SyncScheduler.swift diff --git a/SignalCoreKit/src/Promises/AnyPromise.swift b/SignalCoreKit/src/Promises/AnyPromise.swift index e4c6a03..f75b974 100644 --- a/SignalCoreKit/src/Promises/AnyPromise.swift +++ b/SignalCoreKit/src/Promises/AnyPromise.swift @@ -158,6 +158,10 @@ public class AnyPromise: NSObject { anyPromise.asVoid() } + public func asAny() -> Promise { + return anyPromise + } + @objc @available(swift, obsoleted: 1.0) public class func when(fulfilled promises: [AnyPromise]) -> AnyPromise { diff --git a/SignalCoreKit/src/Promises/Guarantee.swift b/SignalCoreKit/src/Promises/Guarantee.swift index d535afc..a9846f8 100644 --- a/SignalCoreKit/src/Promises/Guarantee.swift +++ b/SignalCoreKit/src/Promises/Guarantee.swift @@ -64,6 +64,34 @@ public extension Guarantee { func asVoid() -> Guarantee { map { _ in } } } +extension Guarantee { + /// Wraps a Swift Concurrency async function in a Guarantee. + /// + /// The Task is created with the default arguments. To configure the task's + /// priority, the caller should create its own Guarantee instance. + public static func wrapAsync(_ block: @escaping () async -> Value) -> Self { + let guarantee = Self() + Task { + guarantee.future.resolve(await block()) + } + return guarantee + } + + /// Converts a Guarantee to a Swift Concurrency async function. + public func awaitable() async -> Value { + await withCheckedContinuation { continuation in + observe(on: SyncScheduler()) { result in + switch result { + case .success(let value): + continuation.resume(returning: value) + case .failure(let error): + owsFail("Unexpectedly received error result from unfailable promise \(error)") + } + } + } + } +} + public extension Guarantee { func map( on scheduler: Scheduler? = nil, diff --git a/SignalCoreKit/src/Promises/Promise.swift b/SignalCoreKit/src/Promises/Promise.swift index 73195fc..7db8955 100644 --- a/SignalCoreKit/src/Promises/Promise.swift +++ b/SignalCoreKit/src/Promises/Promise.swift @@ -82,6 +82,33 @@ public extension Promise { } } +extension Promise { + /// Wraps a Swift Concurrency async function in a Promise. + /// + /// The Task is created with the default arguments. To configure the task's + /// priority, the caller should create its own Promise instance. + public static func wrapAsync(_ block: @escaping () async throws -> Value) -> Self { + let promise = Self() + Task { + do { + promise.future.resolve(try await block()) + } catch { + promise.future.reject(error) + } + } + return promise + } + + /// Converts a Promise to a Swift Concurrency async function. + public func awaitable() async throws -> Value { + try await withCheckedThrowingContinuation { continuation in + self.observe(on: SyncScheduler()) { result in + continuation.resume(with: result) + } + } + } +} + public extension Promise { class func pending() -> (Promise, Future) { let promise = Promise() diff --git a/SignalCoreKit/src/Promises/SyncScheduler.swift b/SignalCoreKit/src/Promises/SyncScheduler.swift new file mode 100644 index 0000000..800914c --- /dev/null +++ b/SignalCoreKit/src/Promises/SyncScheduler.swift @@ -0,0 +1,47 @@ +// +// Copyright 2023 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only +// + +import Foundation + +/// Scheduler that always runs any work synchronously on the current thread. +/// Useful if you don't care what thread your work runs on and want to incur +/// the least overhead. Should NOT be used for scheduled methods, e.g.: +/// `promise.after(seconds: 10, on: SyncScheduler()` would be a bad form and +/// fall back to scheduling in the future on the main thread. +public class SyncScheduler: Scheduler { + + public init() {} + + public func async(_ work: @escaping () -> Void) { + work() + } + + public func sync(_ work: () -> Void) { + work() + } + + public func sync(_ work: () throws -> T) rethrows -> T { + return try work() + } + + public func sync(_ work: () -> T) -> T { + return work() + } + + public func asyncAfter(deadline: DispatchTime, _ work: @escaping () -> Void) { + owsFailDebug("Should not schedule on async queue. Using main queue instead.") + DispatchQueue.main.asyncAfter(deadline: deadline, work) + } + + public func asyncAfter(wallDeadline: DispatchWallTime, _ work: @escaping () -> Void) { + owsFailDebug("Should not schedule on async queue. Using main queue instead.") + DispatchQueue.main.asyncAfter(wallDeadline: wallDeadline, work) + } + + public func asyncIfNecessary(execute work: @escaping () -> Void) { + work() + } +} + diff --git a/SignalCoreKitTests/src/PromiseTests.swift b/SignalCoreKitTests/src/PromiseTests.swift index 262ce0b..814d63a 100644 --- a/SignalCoreKitTests/src/PromiseTests.swift +++ b/SignalCoreKitTests/src/PromiseTests.swift @@ -302,4 +302,16 @@ class PromiseTests: XCTestCase { XCTAssert(doneCalled) XCTAssertEqual(try future.result?.get(), 10) } + + func test_asyncAwait() async throws { + let v1 = try await Promise.wrapAsync { await self.arbitraryAsyncAction() }.awaitable() + XCTAssertEqual(v1, 42) + let v2 = await Guarantee.wrapAsync { await self.arbitraryAsyncAction() }.awaitable() + XCTAssertEqual(v2, 42) + } + + private func arbitraryAsyncAction() async -> Int { + await Task.yield() + return 42 + } }