graphql-react
Version:
A GraphQL client for React using modern context and hooks APIs that’s lightweight (< 4 kB) but powerful; the first Relay and Apollo alternative with server side rendering.
136 lines (110 loc) • 4.27 kB
JavaScript
import Cache from "./Cache.mjs";
import Loading from "./Loading.mjs";
import cacheEntrySet from "./cacheEntrySet.mjs";
/**
* Controls a loading [cache value]{@link CacheValue}.
* @kind class
* @name LoadingCacheValue
* @param {Loading} loading Loading to update.
* @param {Cache} cache Cache to update.
* @param {CacheKey} cacheKey Cache key.
* @param {Promise<CacheValue>} loadingResult Resolves the loading result (including any loading errors) to be set as the [cache value]{@link CacheValue} if loading isn’t aborted. Shouldn’t reject.
* @param {AbortController} abortController Aborts this loading and skips setting the loading result as the [cache value]{@link CacheValue}. Has no effect after loading ends.
* @fires Loading#event:start
* @fires Cache#event:set
* @fires Loading#event:end
* @example <caption>How to import.</caption>
* ```js
* import LoadingCacheValue from "graphql-react/LoadingCacheValue.mjs";
* ```
*/
export default class LoadingCacheValue {
constructor(loading, cache, cacheKey, loadingResult, abortController) {
if (!(loading instanceof Loading))
throw new TypeError("Argument 1 `loading` must be a `Loading` instance.");
if (!(cache instanceof Cache))
throw new TypeError("Argument 2 `cache` must be a `Cache` instance.");
if (typeof cacheKey !== "string")
throw new TypeError("Argument 3 `cacheKey` must be a string.");
if (!(loadingResult instanceof Promise))
throw new TypeError(
"Argument 4 `loadingResult` must be a `Promise` instance."
);
if (!(abortController instanceof AbortController))
throw new TypeError(
"Argument 5 `abortController` must be an `AbortController` instance."
);
/**
* When this loading started.
* @kind member
* @name LoadingCacheValue#timeStamp
* @type {HighResTimeStamp}
*/
this.timeStamp = performance.now();
/**
* Aborts this loading and skips setting the loading result as the
* [cache value]{@link CacheValue}. Has no effect after loading ends.
* @kind member
* @name LoadingCacheValue#abortController
* @type {AbortController}
*/
this.abortController = abortController;
if (!(cacheKey in loading.store)) loading.store[cacheKey] = new Set();
const loadingSet = loading.store[cacheKey];
// In this constructor the instance must be synchronously added to the cache
// key’s loading set, so instances are set in the order they’re constructed
// and the loading store is updated for sync code following construction of
// a new instance.
let loadingAddedResolve;
const loadingAdded = new Promise((resolve) => {
loadingAddedResolve = resolve;
});
/**
* Resolves the loading result, after the [cache value]{@link CacheValue}
* has been set if the loading wasn’t aborted. Shouldn’t reject.
* @kind member
* @name LoadingCacheValue#promise
* @type {Promise<*>}
*/
this.promise = loadingResult.then(async (result) => {
await loadingAdded;
if (
// The loading wasn’t aborted.
!this.abortController.signal.aborted
) {
// Before setting the cache value, await any earlier loading for the
// same cache key to to ensure events are emitted in order and that the
// last loading sets the final cache value.
let previousPromise;
for (const loadingCacheValue of loadingSet.values()) {
if (loadingCacheValue === this) {
// Harmless to await if it doesn’t exist.
await previousPromise;
break;
}
previousPromise = loadingCacheValue.promise;
}
cacheEntrySet(cache, cacheKey, result);
}
loadingSet.delete(this);
if (!loadingSet.size) delete loading.store[cacheKey];
loading.dispatchEvent(
new CustomEvent(`${cacheKey}/end`, {
detail: {
loadingCacheValue: this,
},
})
);
return result;
});
loadingSet.add(this);
loadingAddedResolve();
loading.dispatchEvent(
new CustomEvent(`${cacheKey}/start`, {
detail: {
loadingCacheValue: this,
},
})
);
}
}