1 | ;
|
2 |
|
3 | const createArgErrorMessageProd = require('../private/createArgErrorMessageProd');
|
4 | const Cache = require('./Cache');
|
5 | const Loading = require('./Loading');
|
6 | const cacheEntrySet = require('./cacheEntrySet');
|
7 |
|
8 | /**
|
9 | * Controls a loading [cache value]{@link CacheValue}.
|
10 | * @kind class
|
11 | * @name LoadingCacheValue
|
12 | * @param {Loading} loading Loading to update.
|
13 | * @param {Cache} cache Cache to update.
|
14 | * @param {CacheKey} cacheKey Cache key.
|
15 | * @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.
|
16 | * @param {AbortController} abortController Aborts this loading and skips setting the loading result as the [cache value]{@link CacheValue}. Has no affect after loading ends.
|
17 | * @fires Loading#event:start
|
18 | * @fires Cache#event:set
|
19 | * @fires Loading#event:end
|
20 | * @example <caption>Ways to `import`.</caption>
|
21 | * ```js
|
22 | * import { LoadingCacheValue } from 'graphql-react';
|
23 | * ```
|
24 | *
|
25 | * ```js
|
26 | * import LoadingCacheValue from 'graphql-react/public/LoadingCacheValue.js';
|
27 | * ```
|
28 | * @example <caption>Ways to `require`.</caption>
|
29 | * ```js
|
30 | * const { LoadingCacheValue } = require('graphql-react');
|
31 | * ```
|
32 | *
|
33 | * ```js
|
34 | * const LoadingCacheValue = require('graphql-react/public/LoadingCacheValue');
|
35 | * ```
|
36 | */
|
37 | module.exports = class LoadingCacheValue {
|
38 | constructor(loading, cache, cacheKey, loadingResult, abortController) {
|
39 | if (!(loading instanceof Loading))
|
40 | throw new TypeError(
|
41 | typeof process === 'object' && process.env.NODE_ENV !== 'production'
|
42 | ? 'Argument 1 `loading` must be a `Loading` instance.'
|
43 | : createArgErrorMessageProd(1)
|
44 | );
|
45 |
|
46 | if (!(cache instanceof Cache))
|
47 | throw new TypeError(
|
48 | typeof process === 'object' && process.env.NODE_ENV !== 'production'
|
49 | ? 'Argument 2 `cache` must be a `Cache` instance.'
|
50 | : createArgErrorMessageProd(2)
|
51 | );
|
52 |
|
53 | if (typeof cacheKey !== 'string')
|
54 | throw new TypeError(
|
55 | typeof process === 'object' && process.env.NODE_ENV !== 'production'
|
56 | ? 'Argument 3 `cacheKey` must be a string.'
|
57 | : createArgErrorMessageProd(3)
|
58 | );
|
59 |
|
60 | if (!(loadingResult instanceof Promise))
|
61 | throw new TypeError(
|
62 | typeof process === 'object' && process.env.NODE_ENV !== 'production'
|
63 | ? 'Argument 4 `loadingResult` must be a `Promise` instance.'
|
64 | : createArgErrorMessageProd(4)
|
65 | );
|
66 |
|
67 | if (!(abortController instanceof AbortController))
|
68 | throw new TypeError(
|
69 | typeof process === 'object' && process.env.NODE_ENV !== 'production'
|
70 | ? 'Argument 5 `abortController` must be an `AbortController` instance.'
|
71 | : createArgErrorMessageProd(5)
|
72 | );
|
73 |
|
74 | /**
|
75 | * When this loading started.
|
76 | * @kind member
|
77 | * @name LoadingCacheValue#timeStamp
|
78 | * @type {HighResTimeStamp}
|
79 | */
|
80 | this.timeStamp = performance.now();
|
81 |
|
82 | /**
|
83 | * Aborts this loading and skips setting the loading result as the
|
84 | * [cache value]{@link CacheValue}. Has no affect after loading ends.
|
85 | * @kind member
|
86 | * @name LoadingCacheValue#abortController
|
87 | * @type {AbortController}
|
88 | */
|
89 | this.abortController = abortController;
|
90 |
|
91 | if (!(cacheKey in loading.store)) loading.store[cacheKey] = new Set();
|
92 |
|
93 | const loadingSet = loading.store[cacheKey];
|
94 |
|
95 | // In this constructor the instance must be synchronously added to the cache
|
96 | // key’s loading set, so instances are set in the order they’re constructed
|
97 | // and the loading store is updated for sync code following construction of
|
98 | // a new instance.
|
99 |
|
100 | let loadingAddedResolve;
|
101 |
|
102 | const loadingAdded = new Promise((resolve) => {
|
103 | loadingAddedResolve = resolve;
|
104 | });
|
105 |
|
106 | /**
|
107 | * Resolves the loading result, after the [cache value]{@link CacheValue}
|
108 | * has been set if the loading wasn’t aborted. Shouldn’t reject.
|
109 | * @kind member
|
110 | * @name LoadingCacheValue#promise
|
111 | * @type {Promise<*>}
|
112 | */
|
113 | this.promise = loadingResult.then(async (result) => {
|
114 | await loadingAdded;
|
115 |
|
116 | if (
|
117 | // The loading wasn’t aborted.
|
118 | !this.abortController.signal.aborted
|
119 | ) {
|
120 | // Before setting the cache value, await any earlier loading for the
|
121 | // same cache key to to ensure events are emitted in order and that the
|
122 | // last loading sets the final cache value.
|
123 |
|
124 | let previousPromise;
|
125 |
|
126 | for (const loadingCacheValue of loadingSet.values()) {
|
127 | if (loadingCacheValue === this) {
|
128 | // Harmless to await if it doesn’t exist.
|
129 | await previousPromise;
|
130 | break;
|
131 | }
|
132 |
|
133 | previousPromise = loadingCacheValue.promise;
|
134 | }
|
135 |
|
136 | cacheEntrySet(cache, cacheKey, result);
|
137 | }
|
138 |
|
139 | loadingSet.delete(this);
|
140 |
|
141 | if (!loadingSet.size) delete loading.store[cacheKey];
|
142 |
|
143 | loading.dispatchEvent(
|
144 | new CustomEvent(`${cacheKey}/end`, {
|
145 | detail: {
|
146 | loadingCacheValue: this,
|
147 | },
|
148 | })
|
149 | );
|
150 |
|
151 | return result;
|
152 | });
|
153 |
|
154 | loadingSet.add(this);
|
155 |
|
156 | loadingAddedResolve();
|
157 |
|
158 | loading.dispatchEvent(
|
159 | new CustomEvent(`${cacheKey}/start`, {
|
160 | detail: {
|
161 | loadingCacheValue: this,
|
162 | },
|
163 | })
|
164 | );
|
165 | }
|
166 | };
|