@apollo/client
Version:
A fully-featured caching GraphQL client.
299 lines (298 loc) • 11.8 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.InternalQueryReference = void 0;
exports.wrapQueryRef = wrapQueryRef;
exports.assertWrappedQueryRef = assertWrappedQueryRef;
exports.getWrappedPromise = getWrappedPromise;
exports.unwrapQueryRef = unwrapQueryRef;
exports.updateWrappedQueryRef = updateWrappedQueryRef;
const equality_1 = require("@wry/equality");
const rxjs_1 = require("rxjs");
const internal_1 = require("@apollo/client/utilities/internal");
const invariant_1 = require("@apollo/client/utilities/invariant");
const QUERY_REFERENCE_SYMBOL = Symbol.for("apollo.internal.queryRef");
const PROMISE_SYMBOL = Symbol.for("apollo.internal.refPromise");
function wrapQueryRef(internalQueryRef) {
return {
[QUERY_REFERENCE_SYMBOL]: internalQueryRef,
[PROMISE_SYMBOL]: internalQueryRef.promise,
};
}
function assertWrappedQueryRef(queryRef) {
(0, invariant_1.invariant)(!queryRef || QUERY_REFERENCE_SYMBOL in queryRef, 27);
}
function getWrappedPromise(queryRef) {
const internalQueryRef = unwrapQueryRef(queryRef);
return internalQueryRef.promise.status === "fulfilled" ?
internalQueryRef.promise
: queryRef[PROMISE_SYMBOL];
}
function unwrapQueryRef(queryRef) {
return queryRef[QUERY_REFERENCE_SYMBOL];
}
function updateWrappedQueryRef(queryRef, promise) {
queryRef[PROMISE_SYMBOL] = promise;
}
const OBSERVED_CHANGED_OPTIONS = [
"context",
"errorPolicy",
"fetchPolicy",
"refetchWritePolicy",
"returnPartialData",
];
class InternalQueryReference {
result;
key = {};
observable;
promise;
subscription;
listeners = new Set();
autoDisposeTimeoutId;
resolve;
reject;
references = 0;
softReferences = 0;
constructor(observable, options) {
this.handleNext = this.handleNext.bind(this);
this.dispose = this.dispose.bind(this);
this.observable = observable;
if (options.onDispose) {
this.onDispose = options.onDispose;
}
this.setResult();
this.subscribeToQuery();
// Start a timer that will automatically dispose of the query if the
// suspended resource does not use this queryRef in the given time. This
// helps prevent memory leaks when a component has unmounted before the
// query has finished loading.
const startDisposeTimer = () => {
if (!this.references) {
this.autoDisposeTimeoutId = setTimeout(this.dispose, options.autoDisposeTimeoutMs ?? 30_000);
}
};
// We wait until the request has settled to ensure we don't dispose of the
// query ref before the request finishes, otherwise we would leave the
// promise in a pending state rendering the suspense boundary indefinitely.
this.promise.then(startDisposeTimer, startDisposeTimer);
}
get disposed() {
return this.subscription.closed;
}
get watchQueryOptions() {
return this.observable.options;
}
reinitialize() {
const { observable } = this;
const originalFetchPolicy = this.watchQueryOptions.fetchPolicy;
const avoidNetworkRequests = originalFetchPolicy === "no-cache" || originalFetchPolicy === "standby";
try {
if (avoidNetworkRequests) {
observable.applyOptions({ fetchPolicy: "standby" });
}
else {
observable.reset();
observable.applyOptions({ fetchPolicy: "cache-first" });
}
if (!avoidNetworkRequests) {
this.setResult();
}
this.subscribeToQuery();
}
finally {
observable.applyOptions({ fetchPolicy: originalFetchPolicy });
}
}
retain() {
this.references++;
clearTimeout(this.autoDisposeTimeoutId);
let disposed = false;
return () => {
if (disposed) {
return;
}
disposed = true;
this.references--;
setTimeout(() => {
if (!this.references) {
this.dispose();
}
});
};
}
softRetain() {
this.softReferences++;
let disposed = false;
return () => {
// Tracking if this has already been called helps ensure that
// multiple calls to this function won't decrement the reference
// counter more than it should. Subsequent calls just result in a noop.
if (disposed) {
return;
}
disposed = true;
this.softReferences--;
setTimeout(() => {
if (!this.softReferences && !this.references) {
this.dispose();
}
});
};
}
didChangeOptions(watchQueryOptions) {
return OBSERVED_CHANGED_OPTIONS.some((option) => option in watchQueryOptions &&
!(0, equality_1.equal)(this.watchQueryOptions[option], watchQueryOptions[option]));
}
applyOptions(watchQueryOptions) {
const { fetchPolicy: currentFetchPolicy } = this.watchQueryOptions;
// "standby" is used when `skip` is set to `true`. Detect when we've
// enabled the query (i.e. `skip` is `false`) to execute a network request.
if (currentFetchPolicy === "standby" &&
currentFetchPolicy !== watchQueryOptions.fetchPolicy) {
this.initiateFetch(this.observable.reobserve(watchQueryOptions));
}
else {
this.observable.applyOptions(watchQueryOptions);
}
return this.promise;
}
listen(listener) {
this.listeners.add(listener);
return () => {
this.listeners.delete(listener);
};
}
refetch(variables) {
return this.initiateFetch(this.observable.refetch(variables));
}
fetchMore(options) {
return this.initiateFetch(this.observable.fetchMore(options));
}
dispose() {
this.subscription.unsubscribe();
}
onDispose() {
// noop. overridable by options
}
handleNext(result) {
switch (this.promise.status) {
case "pending": {
// Maintain the last successful `data` value if the next result does not
// have one.
// TODO: This can likely be removed once
// https://github.com/apollographql/apollo-client/issues/12667 is fixed
if (result.data === void 0) {
result.data = this.result.data;
if (result.data) {
result.dataState = "complete";
}
}
if (this.shouldReject(result)) {
this.reject?.(result.error);
}
else {
this.result = result;
this.resolve?.(result);
}
break;
}
default: {
// This occurs when switching to a result that is fully cached when this
// class is instantiated. ObservableQuery will run reobserve when
// subscribing, which delivers a result from the cache.
if (result.data === this.result.data &&
result.networkStatus === this.result.networkStatus) {
return;
}
// Maintain the last successful `data` value if the next result does not
// have one.
if (result.data === void 0) {
result.data = this.result.data;
}
if (this.shouldReject(result)) {
this.promise = (0, internal_1.createRejectedPromise)(result.error);
this.deliver(this.promise);
}
else {
this.result = result;
this.promise = (0, internal_1.createFulfilledPromise)(result);
this.deliver(this.promise);
}
break;
}
}
}
deliver(promise) {
this.listeners.forEach((listener) => listener(promise));
}
initiateFetch(returnedPromise) {
this.promise = this.createPendingPromise();
this.promise.catch(() => { });
// If the data returned from the fetch is deeply equal to the data already
// in the cache, `handleNext` will not be triggered leaving the promise we
// created in a pending state forever. To avoid this situation, we attempt
// to resolve the promise if `handleNext` hasn't been run to ensure the
// promise is resolved correctly.
returnedPromise
.then(() => {
// In the case of `fetchMore`, this promise is resolved before a cache
// result is emitted due to the fact that `fetchMore` sets a `no-cache`
// fetch policy and runs `cache.batch` in its `.then` handler. Because
// the timing is different, we accidentally run this update twice
// causing an additional re-render with the `fetchMore` result by
// itself. By wrapping in `setTimeout`, this should provide a short
// delay to allow the `QueryInfo.notify` handler to run before this
// promise is checked.
// See https://github.com/apollographql/apollo-client/issues/11315 for
// more information
setTimeout(() => {
if (this.promise.status === "pending") {
// Use the current result from the observable instead of the value
// resolved from the promise. This avoids issues in some cases where
// the raw resolved value should not be the emitted value, such as
// when a `fetchMore` call returns an empty array after it has
// reached the end of the list.
//
// See the following for more information:
// https://github.com/apollographql/apollo-client/issues/11642
this.result =
this.observable.getCurrentResult();
this.resolve?.(this.result);
}
});
})
.catch((error) => this.reject?.(error));
return returnedPromise;
}
subscribeToQuery() {
this.subscription = this.observable
.pipe((0, rxjs_1.filter)((result) => !(0, equality_1.equal)(result, this.result)))
.subscribe(this.handleNext);
// call `onDispose` when the subscription is finalized, either because it is
// unsubscribed as a consequence of a `dispose` call or because the
// ObservableQuery completes because of a `ApolloClient.stop()` call.
this.subscription.add(this.onDispose);
}
setResult() {
const result = this.observable.getCurrentResult();
if ((0, equality_1.equal)(result, this.result)) {
return;
}
this.result = result;
this.promise =
result.data ?
(0, internal_1.createFulfilledPromise)(result)
: this.createPendingPromise();
}
shouldReject(result) {
const { errorPolicy = "none" } = this.watchQueryOptions;
return result.error && errorPolicy === "none";
}
createPendingPromise() {
return (0, internal_1.decoratePromise)(new Promise((resolve, reject) => {
this.resolve = resolve;
this.reject = reject;
}));
}
}
exports.InternalQueryReference = InternalQueryReference;
//# sourceMappingURL=QueryReference.cjs.map