2

I have a recursive, async function that queries Google Drive for a file ID using the REST api and a completion handler:

func queryForFileId(query: GTLRDriveQuery_FilesList,
                    handler: @escaping FileIdCompletionHandler) {
    service.executeQuery(query) { ticket, data, error in
        if let error = error {
            handler(nil, error)
        } else {
            let list = data as! GTLRDrive_FileList 
            if let pageToken = list.nextPageToken {
                query.pageToken = pageToken
                self.queryForFileId(query: query, handler: handler)
            } else if let id = list.files?.first?.identifier {
                handler(id, nil)
            } else {
                handler(nil, nil) // no file found
            }
        }
    }
}

Here, query is set up to return the nextPageToken and files(id) fields, service is an instance of GTLRDriveService, and FileIdCompletionHandler is just a typealias:

typealias FileIdCompletionHandler = (String?, Error?) -> Void

I've read how to convert async functions into promises (as in this thread) but I don't see how that can be applied to a recursive, async function. I guess I can just wrap the entire method as a Promise:

private func fileIdPromise(query: GTLRDriveQuery_FilesList) -> Promise<String?> {
    return Promise { fulfill, reject in
        queryForFileId(query: query) { id, error in
            if let error = error {
                reject(error)
            } else {
                fulfill(id)
            }
        }
    }
}

However, I was hoping to something a little more direct:

private func queryForFileId2(query: GTLRDriveQuery_FilesList) -> Promise<String?> {
    return Promise { fulfill, reject in
        service.executeQuery(query) { ticket, data, error in
            if let error = error {
                reject(error)
            } else {
                let list = data as! GTLRDrive_FileList
                if let pageToken = list.nextPageToken {
                    query.pageToken = pageToken
                    // WHAT DO I DO HERE?
                } else if let id = list.files?.first?.identifier {
                    fulfill(id)
                } else {
                    fulfill(nil) // no file found
                }
            }
        }
    }
}

So: what would I do when I need to make another async call to executeQuery?

Ted Hopp
  • 232,168
  • 48
  • 399
  • 521
  • Using `reject` when the file is not found means that I will have to use the error handling code to handle the perfectly normal case of no file being found. That seems like more of a distortion than testing whether the promise was fulfilled with or without a file being found. I'd rather leave the error handling code to deal with things like network failures, permission problems, etc. – Ted Hopp Jun 27 '17 at 19:31

1 Answers1

1

If you want to satisfy a recursive set of promises, at where your "WHAT DO I DO HERE?" line, you'd create a new promise.then {...}.else {...} pattern, calling fulfill in the then clause and reject in the else clause. Obviously, if no recursive call was needed, though, you'd just fulfill directly.

I don't know the Google API and you didn't share your code for satisfying a promise for a list of files, so I'll have to keep this answer a bit generic: Let's assume you had some retrieveTokens routine that returned a promise that is satisfied only when all of the promises for the all files was done. Let's imagine that the top level call was something like:

retrieveTokens(for: files).then { tokens in
    print(tokens)
}.catch { error in
    print(error)
}

You'd then have a retrieveTokens that returns a promise that is satisfied only when then promises for the individual files were satisfied. If you were dealing with a simple array of File objects, you might do something like:

func retrieveTokens(for files: [File]) -> Promise<[Any]> {
    var fileGenerator = files.makeIterator()
    let generator = AnyIterator<Promise<Any>> {
        guard let file = fileGenerator.next() else { return nil }
        return self.retrieveToken(for: file)
    }

    return when(fulfilled: generator, concurrently: 1)
}

(I know this isn't what yours looks like, but I need this framework to show my answer to your question below. But it’s useful to encapsulate this “return all promises at a given level” in a single function, as it allows you to keep the recursive code somewhat elegant, without repeating code.)

Then the routine that returns a promise for an individual file would see if a recursive set of promises needed to be returned, and put its fulfill inside the then clause of that new recursively created promise:

func retrieveToken(for file: File) -> Promise<Any> {
    return Promise<Any> { fulfill, reject in
        service.determineToken(for: file) { token, error in
            // if any error, reject

            guard let token = token, error == nil else {
                reject(error ?? FileError.someError)
                return
            }

            // if I don't have to make recursive call, `fulfill` immediately.
            // in my example, I'm going to see if there are subfiles, and if not, `fulfill` immediately.

            guard let subfiles = file.subfiles else {
                fulfill(token)
                return
            }

            // if I got here, there are subfiles and I'm going to start recursive set of promises

            self.retrieveTokens(for: subfiles).then { tokens in
                fulfill(tokens)
            }.catch { error in
                reject(error)
            }
        }
    }
}

Again, I know that the above isn't a direct answer to your question (as I'm not familiar with Google Drive API nor how you did your top level promise logic). So, in my example, I created model objects sufficient for the purposes of the demonstration.

But hopefully it's enough to illustrate the idea behind a recursive set of promises.

Rob
  • 415,655
  • 72
  • 787
  • 1,044