-
Notifications
You must be signed in to change notification settings - Fork 61
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
ApolloPagination: Various API Improvements, refactor into actor
#80
ApolloPagination: Various API Improvements, refactor into actor
#80
Conversation
13919b6
to
552c689
Compare
actor
apollo-ios-pagination/Sources/ApolloPagination/GraphQLQueryPager.swift
Outdated
Show resolved
Hide resolved
apollo-ios-pagination/Sources/ApolloPagination/AnyGraphQLPager.swift
Outdated
Show resolved
Hide resolved
@@ -1,160 +1,205 @@ | |||
import Apollo | |||
import ApolloAPI | |||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we add a function here for easily converting a GraphQLPager
to an AnyGraphQLPager
like a map
function? Not sure what to call it, maybe mappingResults
?
So you can do this:
let pager: AnyGraphQLPager<CustomModel> = makeQueryPager(
client: client,
queryProvider: { _ in PageQuery() },
extractPageInfo: { data in CustomPage(data) }
).mappingResults { result in
CustomModel(result)
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have those over in AnyGraphQLPager.swift
-- extensions on GraphQLQueryPager
named eraseToAnyPager
, which allow you to supply a function mapping Output
to your custom model.
apollo-ios-pagination/Sources/ApolloPagination/GraphQLQueryPager.swift
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is coming along so nicely! Thanks!
Co-authored-by: Anthony Miller <anthonymdev@gmail.com>
@Iron-Ham any idea why the CI tests etc arent running anymore for this PR? Doesn't look like the additions you made should have affected that but maybe they did? |
@BobaFetters No clue -- the CI changes in here shouldn't have an impact. I imagine it's related to the verified commit accepting Anthony's suggestion? I can double check when I'm back in office 😜 |
…apollo-ios-dev into hs/support-throwing-transforms
c618c8d8 ApolloPagination: Various API Improvements, refactor into (#80) git-subtree-dir: apollo-ios-pagination git-subtree-split: c618c8d8fcbafa73f23484e79e03dc02d000c072
… Improvements, refactor into git-subtree-dir: apollo-ios-pagination git-subtree-mainline: b5daee0 git-subtree-split: c618c8d8fcbafa73f23484e79e03dc02d000c072
What are you trying to accomplish?
loadMore
across many threads.AnyGraphQLQueryPager
s.canLoadNext
function that can be properly used in a type erased pager.AnyGraphQLQueryPager
to operate over an individual model as well as arrays. I found that some instances of pagination involved updating a singular model, vs keeping an array of models.UIKit
andSwiftUI
contexts.What approach did you choose and why?
Thread Safety
I kept bashing my head into this towards the tail end of last week. On the morning of 10/15, the idea just came to me:
Instead of trying to use
async/await
APIs, or locks, or what have you, you can use a combination ofCombine
andasync/await
conversion APIs to achieve the goal.This API is deceptively challenging and non-trivial to make thread safe. It boils down to the following set of requirements:
loadMore
action if there is already an extantloadMore
in flight.loadMore
isn't complete until it triggers the callback of its underlying watcher.cachePolicy
), and then again if it observes any cache changes. This means that we want to block it only until the "canonical" load has landed.A continuation can only be continued once. Continuing it more than once will trigger a crash.
A Combine Publisher can continuously receive values, until it receives a completion event, at which point it will no longer receive or publish values.
My thought boiled down to the following:
loadMore
function is called again.By then turning the Pager into an actor, we can ensure thread safety as it runs all of its operations in a thread isolated context
Support for throwing transforms in type erased
AnyGraphQLQueryPager
sSome models don't necessarily always initialize. I'm choosing to support throwing transforms here as opposed to
(Query.Data) -> ModelType?
with the assumption that one can be converted to the other, but throwing transforms provide additional information that can be bubbled up to the pager.Changes the API to use a
canLoadNext
function that can be properly used in a type erased pager.When used as a variable, we couldn't access this value as it required knowledge of the pager used during instantiation. However, as a function, we can pass its reference to the type erasure and access it accordingly.
Allow
AnyGraphQLQueryPager
to operate over an individual model as well as arrays.I found that some instances of pagination involved updating a singular model, vs keeping an array of models. Some of our code at GitHub operates over a single model. For example, when rendering a pull request, we operate over a pull request model. When we paginate in that view, we are loading the events, comments, and so on that are associated with that pull request (
@so-and-so
has marked as draft,@so-and-so-and-so
has closed with comment, etc.).Ease of use for both
UIKit
andSwiftUI
contexts.The
GraphQLQueryPager
is easily used inUIKit
, whereasSwiftUI
can easily use bothGraphQLQueryPager
andGraphQLQueryPager.Actor
(.task { ... }
can use an actor easily).Anything you want to highlight for special attention from reviewers?
Yes! This is pretty wild stuff. I would love a second set of eyes on this.
🤖 Generated by Copilot
Summary
🔄🚀🐛
This pull request introduces a new
GraphQLQueryPagerWrapper
class that adds concurrency and cancellation support to the existingGraphQLQueryPager
class for pagination of GraphQL queries. It also updates various classes and tests that use theGraphQLQueryPager
class to use the new wrapper class instead. Additionally, it refactors the type-erasingAnyGraphQLQueryPager
class to use the new wrapper class and simplifies the pagination logic and interface. Finally, it adds a new error caseloadInProgress
to thePaginationError
enum to handle concurrent load more requests.Walkthrough
GraphQLQueryPager
type withGraphQLQueryPagerWrapper
type in various pagination scenarios and testsloadInProgress
to thePaginationError
enumtest_concurrentFetches_nonisolated
to theConcurrencyTests
classfetch
andloadMore
methods of theGraphQLQueryPager
class into smaller methods that handle the initial fetch, the subsequent fetch, and the task cancellationsubscribe
method from theGraphQLQueryPager
class and theAnyGraphQLQueryPager
classfetch
method from theGraphQLQueryPager
class and theAnyGraphQLQueryPager
class_subject
property and thesubject
property from theAnyGraphQLQueryPager
class_subject.send(completion: .finished)
line from thecancel
method of theGraphQLQueryPager
classAnyGraphQLQueryPagerTests
class