maticzav / swift-graphql

A GraphQL client that lets you forget about GraphQL.
https://swift-graphql.com
MIT License
606 stars 67 forks source link

Support for async/await #51

Closed jacobhzen closed 1 year ago

jacobhzen commented 3 years ago

Support for Swift 5.5's new async/await would be amazing.

NeverwinterMoon commented 3 years ago

Yep, I would love that, too. Now it’s official: async/await backward compatible down to iOS 13: https://developer.apple.com/documentation/xcode-release-notes/xcode-13_2-release-notes

Amzd commented 3 years ago

This should be as simple as wrapping the send function with withCheckedThrowingContinuation. Don’t think it applies to the websockets.

This is a good task for a first time contributor. Just create a new file HTTP+async.swift and have two async send functions which wrap their counterparts from the HTTP.swift file.

Amzd commented 3 years ago

Correction; We can implement AsyncSequence for web sockets: https://obscuredpixels.com/awaiting-websockets-in-swiftui

But this is not as trivial as the send functions so I would make this a separate issue.

maticzav commented 2 years ago

Hi everyone πŸ‘‹ ,

I am happy to co-work on this feature now that the new version is out. Let me know if anyone wants to give it a try!

elfenlaid commented 2 years ago

@maticzav Sounds interesting! I would like to give it a try πŸ™

Tho, seems a lot changed since the initial discussion had kicked off. Please correct me if I'm wrong, but it seems that introducing async/await in the current version's context means extending the following functions with async analogous:

public class Client: GraphQLClient, ObservableObject {
    // ...
    public func query(
        _ args: ExecutionArgs,
        request: URLRequest? = nil,
        policy: Operation.Policy = .cacheFirst
    ) -> Source {
       // ...
    }

    // ...
    public func mutate(
        _ args: ExecutionArgs,
        request: URLRequest? = nil,
        policy: Operation.Policy = .cacheFirst
    ) -> Source {
    // ...
    }

    // ...
    public func subscribe(
        _ args: ExecutionArgs,
        request: URLRequest? = nil,
        policy: Operation.Policy = .cacheFirst
    ) -> Source {
    // ...
    }
}

Also I must note that Combine provides a binding to async/await out-of-box:

let publisher = remoteDataPublisher(forURLs: urls)

for try await data in publisher.values { // AsyncThrowingSequence
    ...
}

(Source ☝️ Async sequences, streams, and Combine | Swift by Sundell)

That said, it seems that we can extend the Client's API by introducing one-shot throwing async versions of query and mutate. As for subscription we can provide a version that returns AsyncThrowingStream as out of convenience πŸ€”

Please let me know what do you think about it πŸ™‡

maticzav commented 2 years ago

@elfenlaid, you are right! Let me know if you are still interested, your comment must have slipped through the cracks of my inbox ☹️

elfenlaid commented 2 years ago

@maticzav yeap, on it then πŸ™‡

elfenlaid commented 1 year ago

I'm so sorry, but I'll park the ticket for now πŸ™‡

pokryfka commented 1 year ago

for my needs I just created an extension

import struct Foundation.URLRequest
import SwiftGraphQL
import SwiftGraphQLClient

public extension GraphQLClient {
    func queryAsync<T, TypeLock>(
        _ selection: Selection<T, TypeLock>,
        as operationName: String? = nil,
        request: URLRequest? = nil,
        policy: Operation.Policy = .cacheFirst
    ) -> AsyncThrowingStream<DecodedOperationResult<T>, any Error> where TypeLock: GraphQLHttpOperation {
        query(selection, as: operationName, request: request, policy: policy)
            .values
            .eraseToThrowingStream()
    }

    func mutateAsync<T, TypeLock>(
        _ selection: Selection<T, TypeLock>,
        as operationName: String? = nil,
        request: URLRequest? = nil,
        policy: Operation.Policy = .cacheFirst
    ) -> AsyncThrowingStream<DecodedOperationResult<T>, Error> where TypeLock: GraphQLHttpOperation {
        mutate(selection, as: operationName, request: request, policy: policy)
            .values
            .eraseToThrowingStream()
    }

    func subscribeAsync<T, TypeLock>(
        to selection: Selection<T, TypeLock>,
        as operationName: String? = nil,
        request: URLRequest? = nil,
        policy: Operation.Policy = .cacheFirst
    ) -> AsyncThrowingStream<DecodedOperationResult<T>, Error> where TypeLock: GraphQLWebSocketOperation {
        subscribe(to: selection, as: operationName, request: request, policy: policy)
            .values
            .eraseToThrowingStream()
    }
}
pokryfka commented 1 year ago

For the record, and in case its useful, somehow I thought eraseToThrowingStream() is part of Swift but I had it in already imported swift-concurrency-extra packaged:

https://github.com/pointfreeco/swift-concurrency-extras/blob/main/Sources/ConcurrencyExtras/AsyncThrowingStream.swift

--

Also note that while in case of mutations arguably cached results should not be used, for queries with policy set to .cacheAndNetwork there may be two values returned, see https://github.com/maticzav/swift-graphql/pull/105#discussion_r1348624662