Skip to content
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

API for client-side field/argument resoluton #431

Closed
johanobergman opened this issue Oct 6, 2015 · 9 comments
Closed

API for client-side field/argument resoluton #431

johanobergman opened this issue Oct 6, 2015 · 9 comments

Comments

@johanobergman
Copy link

Use case: I have a calendar with events, for which I have a query with two arguments, startDate and endDate.

events(startDate: 2015-01-01, endDate: 2015-12-31) {
    ...
}

Sometimes, I have to expand or narrow the range, or completely change it, all based on user input. But everytime Relay sees a pair of arguments it hasn't seen before, it goes over the network to fetch the data.

If the arguments (startDate: 2015-01-01, endDate: 2015-03-01) are changed to (startDate: 2015-02-01, endDate: 2015-02-20), ideally no fetch would be needed. It would be great if we had an API that could teach Relay how to handle the arguments on a case by case basis. I thought about this myself for a while and came up with the following API (forgive me for being pretty ignorant of Relay's internals).

/**
 * Register handlers for when the arguments 'startDate'
 * and 'endDate' are used on a node named 'events'.
 * The handlers should compute the best args for the situation
 * and make sure to re-use existing records whenever possible.
 */
Relay.onArguments(
    'events',
    ['startDate', 'endDate'],
    prepareQueryArgs,
    mergeRecordsToNewQueryCache
);

/**
 * Get the minimum set of args required to fetch new data.
 * Return false if everything already is in the cache.
 * 
 * @param  {object} args       The arguments of the query.
 * @param  {object} queryCache Some way to reach into Relays cached queries.
 * @return {array | object | false} The computed arguments, or an array of them.
 */
function prepareQueryArgs(args, queryCache) {
    // My usecase means finding all date intervals where no data has been fetched.
    let intervals = queryCache.viewer.events.map(query =>
        makeInterval(query.args.startDate, query.args.endDate));

    let missingIntervals = diffIntervals(args.startDate, args.endDate, intervals);

    // We've already got everything in the cache, so no fetch needed.
    if (!missingIntervals.length) return false;

    // If there are multiple sets of arguments, Relay
    // should make fragments for each one of the sets.
    let newArgs = missingIntervals.map(i => ({startDate: i.start, endDate: i.end}));

    return newArgs;
}

/**
 * Make a new query cache entry by combining the cached records which
 * conform to the arguments with the ones that came from the fetch.
 * 
 * @param  {object} args
 * @param  {array}  cache
 * @param  {array}  response
 * @return {array}
 */
function mergeRecordsToNewQueryCache(args, cache, response) {
    return cache.filter(record => record.type === 'Event' &&
        record.date >= args.startDate && record.date <= args.endDate)
        .concat(response); // I do all further sorting client side, so concat is enough here.
}

I'd love to see something like this get implemented, and I'm sure it covers many use cases.

@josephsavona josephsavona changed the title API for making more efficient fetches with args API for client-side field/argument resoluton Oct 6, 2015
@josephsavona
Copy link
Contributor

@johanobergman Thanks for posting! This is one example of a larger pattern of use-cases where certain argument values may describe a strict subset of data that has already been fetched. In these cases there is an opportunity to allow clients to inject some amount of custom behavior and potentially "resolve" calls locally.

Some challenges include determining an appropriate API and managing access to the Relay store (so that applications can inspect what data has been fetched). I'm going to mark this as an enhancement so it stays on our radar.

@nickretallack
Copy link

I wonder if it would be possible for you to implement this using connections and cursors?

@josephsavona
Copy link
Contributor

@nickretallack The OP is using connections and cursors. The issue here is that Relay treats field arguments as opaque values and can't automatically optimize for the fact that the results for some argument values may be strict subsets of the result of already cached argument values. E.g. the items for March 1 - April 1 don't need to be fetched from the server if Jan 1 - Dec 31 are already fetched.

@johanobergman
Copy link
Author

I have another question. What if I add a new edge to a connection with arguments like the ones above? I might have had multiple fetches with different dates as arguments. Can I somehow tell rangeBehaviors to add it to all caches where the new edge meet the date criteria?

@nickretallack
Copy link

I meant if you made the startDate be a cursor value instead of an argument. Not sure what you'd do with the endDate though, or how relay could be smarter about handling it. Just putting forward a more generic way of modeling the problem.

@josephsavona
Copy link
Contributor

I have another question

@johanobergman - Would you mind posting this second question to Stack Overflow? Doing so keeps the issues here targeted around bugs and enhancements, while Stack Overflow provides an experience tailored for general Q&A and a wider audience to help get your question answered faster. Thank you!

@taion
Copy link
Contributor

taion commented Nov 30, 2015

This reminds me a lot of a related issue of being able to have something like "typed nodes", where e.g. if I am fetching query { widget(widget: $widgetId) }, I should also be able to resolve that locally if the relevant node is already available locally (e.g. query { node(type: 'widget', localId: $widgetId) }).

@josephsavona
Copy link
Contributor

Some brief notes about how I'd approach this architecturally (in the new core #1369):

  • Fields can be annotated with a directive (e.g. @relay(resolver: "foo")
  • When a query is fetched with caching enabled (not force-fetch), execute the query against the cache.
    • If a field is missing and it does not have the @relay(resolver) directive, the field is missing and the query cannot be fulfilled from cache: fall back to the server
    • If a field is missing and it does have the directive, lookup the value of the "resolver" property in a user-defined table of field resolver functions. Throw if user failed to specify a function. Otherwise execute the function with some context (the record/field/args in question) and allow the resolver function to write data into the store for that field. After calling the resolver, resume query execution: if the resolver was able to populate a value then data will be there and the query may be able to resolve from cache: if the field is still missing it means the resolver couldn't populate a value.

@leebyron
Copy link
Contributor

I'm sorry this issue has sat here for so long. In an attempt to clean up our issue queue we're closing some aging or unclear-action issues. Also, while this issue applies to Relay Classic, our team is trying to focus on Relay Modern going forward.

If this is something you'd like to see happen, then we'd be happy to review a pull request.

If this issue is still important to you, please feel free to reopen it with an update.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

5 participants