Skip to content
This repository has been archived by the owner on Jun 26, 2024. It is now read-only.

Commit

Permalink
Add async-await support to Promises
Browse files Browse the repository at this point in the history
  • Loading branch information
max-signal authored Sep 25, 2023
1 parent da1f1fd commit ea99e90
Show file tree
Hide file tree
Showing 5 changed files with 118 additions and 0 deletions.
4 changes: 4 additions & 0 deletions SignalCoreKit/src/Promises/AnyPromise.swift
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,10 @@ public class AnyPromise: NSObject {
anyPromise.asVoid()
}

public func asAny() -> Promise<Any> {
return anyPromise
}

@objc
@available(swift, obsoleted: 1.0)
public class func when(fulfilled promises: [AnyPromise]) -> AnyPromise {
Expand Down
28 changes: 28 additions & 0 deletions SignalCoreKit/src/Promises/Guarantee.swift
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,34 @@ public extension Guarantee {
func asVoid() -> Guarantee<Void> { 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<T>(
on scheduler: Scheduler? = nil,
Expand Down
27 changes: 27 additions & 0 deletions SignalCoreKit/src/Promises/Promise.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<Value>, Future<Value>) {
let promise = Promise<Value>()
Expand Down
47 changes: 47 additions & 0 deletions SignalCoreKit/src/Promises/SyncScheduler.swift
Original file line number Diff line number Diff line change
@@ -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<T>(_ work: () throws -> T) rethrows -> T {
return try work()
}

public func sync<T>(_ 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()
}
}

12 changes: 12 additions & 0 deletions SignalCoreKitTests/src/PromiseTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}

0 comments on commit ea99e90

Please sign in to comment.