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

feat: Scoped Queries #1544

Merged
merged 133 commits into from
Apr 21, 2024
Merged

feat: Scoped Queries #1544

merged 133 commits into from
Apr 21, 2024

Conversation

vicary
Copy link
Member

@vicary vicary commented Mar 4, 2023

Current Progress
  1. Batch queries via microtask
  2. Rework examples/ into React and Vite, add Svelete and Vue later.
  3. Rework canary action to allow custom --pre stages
  4. Move jest to workspace root
  5. Remove schema if the legacy accessors still works as intended
    1. Freeze proxy accessors after context resolve.
    2. Before resolving, accessors will try to read from the cache.
    3. After resolving, accessors essentially become plain objects.
    4. Legacy global accessors never resolve, so it's backward compatible.
New CLI New React
Screen_Recording_2023-04-06_at_21.40.15.mov
Screen_Recording_2023-04-07_at_20.24.16.mov

Scoped Queries

The core client has recently gone through a major overhaul, the underlying mechanism is largely rewritten.

To ensure a smooth and progressive migration path, exposed functions are rebuilt with new parts with the API unchanged. If you find a behavioural change and you are not yet ready to fully migrate, please open an issue in GitHub.

Sketch - Scoped Query

image

Why

The main reason for this overhual is to introduce isolated scoped context for queries and caches. It serves as an extension point for many upcoming features, in fact some of them has already landed here!

What

tl;dr: Axing, pruning and reshaping. A lot of them.

This change removed all centralized internal components, namely, the Scheduler, the Interceptor and the EventHandler.

The new core exposes only two functions, resovle and subscribe. They cover most, if not all, of the previous API usage.

resolve is a promise based API, built with scoped queries such as fetchPolicy and operationName in mind. It covers the features of resolved and inlineResolved. Subscriptions invoked via resolve will be unsubscribed on the first received message for a promise-like experience.

subscribe is an async generator, yielding cache changes from subscriptions or other queries. It covers the original track feature, while sending the initiating request. When query or mutation are used here, it will not return upon response. Instead, it keeps listening to cache changes which may be invoked elsewhere.

Both resolve and subscribe does not restrict the usage of query, mutation or subscription. The only difference is subscribe streams cache changes, and resolve is a one-off query.

Cache Policy

The existing fetchPolicy option is an incomplete copy of the Apollo Client's version, with different functions supporting a different subset of the options.

GQty needs an option which make sense across all exposed methods, where users only need to learn once.

I introduced a new cachePolicy option, heavily inspired by three popular sources, React Query, Apollo Client and WHATWG.

Following Pablo's theme of ESM and web standards, I chose WHATWG's version as the main blueprint. Each of the options are explain below.

  1. default: Similar to Apollo's cache-first and cache-and-network combined. It serves the cached contents when it is fresh, and if they are stale within the staleWhileRevalidate window, fetches in the background and updates the cache. Or simply fetches on stale cache or cache miss. During SWR, a successful fetch will not notify cache updates. New contents are served on next query.
  2. no-store: Similar to Apollo's network-only. It always fetch and does not update on response. GQty creates a temporary cache at query-level which immediately expires.
  3. no-cache: Always fetch, updates on response.
  4. force-cache: Serves the cached contents regardless of staleness. It fetches on cache miss or a stale cache, updates cache on response.
  5. only-if-cached: Similar to Apollo's cache-only. It serves the cached contents regardless of staleness, throws a network error on cache miss.
Sketch - Grand Unified Query & Fetch Policy

image

Query Deduplication

The active state of query fetches and subscription connections are now kept and deduped on a cache level, preventing excessive network requests on the exact same resources.

Subsequent or duplicated request to ongoing queries gives you the same fetch promise.

More on Subscriptions

Previously, our subscription client was a modified version of mercurius. Due to implementation details, some of the existing issues are impossible without a hacky workaround.

GQty now accepts a subscription client interface shared by graphql-ws and graphql-sse, you may easily swap between them or implement your own. The generated client now imports graphql-ws when subscriptions are enabled during codegen, where it should be installed separately.

Most popular GraphQL servers such as Apollo Server, Mercurius already support both protocols out of the box. If you are using these servers, no changes are required.

The Cache

The cache has been refactored for extensibility, new features such as cache expiry and stale-while-relvidate are one of the first candidates who benefited from it.

Sketch - Cache GC

image

Cache expiry and stale-while-revalidate

If you are a user of React Query, the new cache options, maxAge and staleWhileRevalidate, works very similar to their cacheTime and staleTime respectively.

The new cache handles expiry a bit different then Apollo Client and React Query. Instead of periodic purges, we use WeakRef to offload eviction to the native GC process. This works nicely when combined with our new fetchPolicy option.

Cache Manipulation

There is a new low-level API method $meta(), which exposes a number of contextual information about the query proxy, one of them being a direct reference to the cache data.

This enables further simplification when it comes to cache manipulation. For example, the setCache() function can be easily replaced with Object.assign() even for query with inputs.

For field level cache updates, the existing syntax of query.me.name = 'Jane Doe'; still works.

Normalization

Generally normalization works by hoisting objects that can be uniquely identified to a global store, related subscribers will be notified if multiple queries are referencing the same object.

Normalization with cache expiry is tricky to get right. In a highly simplified scenario, you would need to resolve conflicts with objects that is identifyable with the same ID in these 3 situations:

  1. When object conflict happens in the same query, it should means the same fresh object and they are all merged into one normalized object.
  2. When object conflict happens between a query and the cache, merging is a common choice when one of them is a strict superset of the other.
  3. When object conflict happens between a query and the cache, but they are not a superset, the incoming one should replace the store.

How

Apart from the main goal of scoped queries, as a secondary side effect of this refactoring, internal components are now less tightly coupled.

Picking the components apart opens up many possibilities in the future. It increases extensibility, improves maintenability and testability.

Axed

  1. Scheduler
  2. Interceptor
  3. Event
  4. cycle.js
  5. @gqty/subscriptions
  6. @gqty/utils

Remade

1. Accessor

The first breaking change is the deprecation of query, mutation and subscription. They are now replaced by a single schema accessor.

The main reason behind this change is to avoid the intrinsic freezing of module level exports. Previously the accessors are exported at the top level from the generated client, locking the scoped context, which prevents new cache contents from being read.

With a proxy shim, these accessors can still read the cache, but they no longer trigger fetches in the background. Network requests must now explicitly made via resolve, subscribe or one of the legacy API functions.

2. Cache

In the process of trimming down the core, half of the runtime validations against the schema are removed. Validation is now done via type hinting and type checking.

The cache will become more like a reactive object that triggers rendering, and even more so in future iterations.

3. Client

4. Selection

@vicary vicary mentioned this pull request Mar 4, 2023
8 tasks
@vicary vicary force-pushed the feat/scoped-query branch 2 times, most recently from c32fcb5 to ffc4103 Compare January 30, 2024 08:30
@vicary vicary merged commit a758ed1 into main Apr 21, 2024
7 checks passed
@vicary vicary deleted the feat/scoped-query branch April 21, 2024 05:23
@github-actions github-actions bot mentioned this pull request Aug 26, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant