UNPKG

16.1 kBJavaScriptView Raw
1/**
2 * Copyright (c) Facebook, Inc. and its affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 *
7 *
8 * @emails oncall+relay
9 * @format
10 */
11'use strict';
12
13var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
14
15var _objectSpread2 = _interopRequireDefault(require("@babel/runtime/helpers/objectSpread"));
16
17var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
18
19var ExecutionEnvironment = require('fbjs/lib/ExecutionEnvironment');
20
21var LRUCache = require('./LRUCache');
22
23var invariant = require("fbjs/lib/invariant");
24
25var _require = require('relay-runtime'),
26 isPromise = _require.isPromise,
27 RelayFeatureFlags = _require.RelayFeatureFlags;
28
29var CACHE_CAPACITY = 1000;
30var DEFAULT_FETCH_POLICY = 'store-or-network';
31var DEFAULT_RENDER_POLICY = RelayFeatureFlags.ENABLE_PARTIAL_RENDERING_DEFAULT === true ? 'partial' : 'full';
32var DATA_RETENTION_TIMEOUT = 30 * 1000;
33
34function getQueryCacheKey(operation, fetchPolicy, renderPolicy) {
35 return "".concat(fetchPolicy, "-").concat(renderPolicy, "-").concat(operation.request.identifier);
36}
37
38function getQueryResult(operation, cacheKey) {
39 var rootFragmentRef = {
40 __id: operation.fragment.dataID,
41 __fragments: (0, _defineProperty2["default"])({}, operation.fragment.node.name, operation.request.variables),
42 __fragmentOwner: operation.request
43 };
44 return {
45 cacheKey: cacheKey,
46 fragmentNode: operation.request.node.fragment,
47 fragmentRef: rootFragmentRef,
48 operation: operation
49 };
50}
51
52function createCacheEntry(cacheKey, operation, value, networkSubscription, onDispose) {
53 var currentValue = value;
54 var retainCount = 0;
55 var permanentlyRetained = false;
56 var retainDisposable = null;
57 var releaseTemporaryRetain = null;
58 var currentNetworkSubscription = networkSubscription;
59
60 var retain = function retain(environment) {
61 retainCount++;
62
63 if (retainCount === 1) {
64 retainDisposable = environment.retain(operation.root);
65 }
66
67 return {
68 dispose: function dispose() {
69 retainCount = Math.max(0, retainCount - 1);
70
71 if (retainCount === 0) {
72 !(retainDisposable != null) ? process.env.NODE_ENV !== "production" ? invariant(false, 'Relay: Expected disposable to release query to be defined.' + "If you're seeing this, this is likely a bug in Relay.") : invariant(false) : void 0;
73 retainDisposable.dispose();
74 retainDisposable = null;
75 }
76
77 onDispose(cacheEntry);
78 }
79 };
80 };
81
82 var cacheEntry = {
83 cacheKey: cacheKey,
84 getValue: function getValue() {
85 return currentValue;
86 },
87 setValue: function setValue(val) {
88 currentValue = val;
89 },
90 getRetainCount: function getRetainCount() {
91 return retainCount;
92 },
93 getNetworkSubscription: function getNetworkSubscription() {
94 return currentNetworkSubscription;
95 },
96 setNetworkSubscription: function setNetworkSubscription(subscription) {
97 if (currentNetworkSubscription != null) {
98 currentNetworkSubscription.unsubscribe();
99 }
100
101 currentNetworkSubscription = subscription;
102 },
103 temporaryRetain: function temporaryRetain(environment) {
104 // NOTE: If we're executing in a server environment, there's no need
105 // to create temporary retains, since the component will never commit.
106 if (!ExecutionEnvironment.canUseDOM) {
107 return {
108 dispose: function dispose() {}
109 };
110 }
111
112 if (permanentlyRetained === true) {
113 return {
114 dispose: function dispose() {}
115 };
116 } // NOTE: temporaryRetain is called during the render phase. However,
117 // given that we can't tell if this render will eventually commit or not,
118 // we create a timer to autodispose of this retain in case the associated
119 // component never commits.
120 // If the component /does/ commit, permanentRetain will clear this timeout
121 // and permanently retain the data.
122
123
124 var disposable = retain(environment);
125 var releaseQueryTimeout = null;
126
127 var localReleaseTemporaryRetain = function localReleaseTemporaryRetain() {
128 clearTimeout(releaseQueryTimeout);
129 releaseQueryTimeout = null;
130 releaseTemporaryRetain = null;
131 disposable.dispose();
132 };
133
134 releaseQueryTimeout = setTimeout(localReleaseTemporaryRetain, DATA_RETENTION_TIMEOUT); // NOTE: Since temporaryRetain can be called multiple times, we release
135 // the previous temporary retain after we re-establish a new one, since
136 // we only ever need a single temporary retain until the permanent retain is
137 // established.
138 // temporaryRetain may be called multiple times by React during the render
139 // phase, as well multiple times by other query components that are
140 // rendering the same query/variables.
141
142 if (releaseTemporaryRetain != null) {
143 releaseTemporaryRetain();
144 }
145
146 releaseTemporaryRetain = localReleaseTemporaryRetain;
147 return {
148 dispose: function dispose() {
149 if (permanentlyRetained === true) {
150 return;
151 }
152
153 releaseTemporaryRetain && releaseTemporaryRetain();
154 }
155 };
156 },
157 permanentRetain: function permanentRetain(environment) {
158 var disposable = retain(environment);
159
160 if (releaseTemporaryRetain != null) {
161 releaseTemporaryRetain();
162 releaseTemporaryRetain = null;
163 }
164
165 permanentlyRetained = true;
166 return {
167 dispose: function dispose() {
168 disposable.dispose();
169
170 if (retainCount <= 0 && currentNetworkSubscription != null) {
171 currentNetworkSubscription.unsubscribe();
172 }
173
174 permanentlyRetained = false;
175 }
176 };
177 }
178 };
179 return cacheEntry;
180}
181
182var QueryResourceImpl =
183/*#__PURE__*/
184function () {
185 function QueryResourceImpl(environment) {
186 var _this = this;
187
188 (0, _defineProperty2["default"])(this, "_clearCacheEntry", function (cacheEntry) {
189 if (cacheEntry.getRetainCount() <= 0) {
190 _this._cache["delete"](cacheEntry.cacheKey);
191 }
192 });
193 this._environment = environment;
194 this._cache = LRUCache.create(CACHE_CAPACITY);
195 }
196 /**
197 * This function should be called during a Component's render function,
198 * to either read an existing cached value for the query, or fetch the query
199 * and suspend.
200 */
201
202
203 var _proto = QueryResourceImpl.prototype;
204
205 _proto.prepare = function prepare(operation, fetchObservable, maybeFetchPolicy, maybeRenderPolicy, observer, cacheKeyBuster) {
206 var _maybeFetchPolicy, _maybeRenderPolicy;
207
208 var environment = this._environment;
209 var fetchPolicy = (_maybeFetchPolicy = maybeFetchPolicy) !== null && _maybeFetchPolicy !== void 0 ? _maybeFetchPolicy : DEFAULT_FETCH_POLICY;
210 var renderPolicy = (_maybeRenderPolicy = maybeRenderPolicy) !== null && _maybeRenderPolicy !== void 0 ? _maybeRenderPolicy : DEFAULT_RENDER_POLICY;
211 var cacheKey = getQueryCacheKey(operation, fetchPolicy, renderPolicy);
212
213 if (cacheKeyBuster != null) {
214 cacheKey += "-".concat(cacheKeyBuster);
215 } // 1. Check if there's a cached value for this operation, and reuse it if
216 // it's available
217
218
219 var cacheEntry = this._cache.get(cacheKey);
220
221 var temporaryRetainDisposable = null;
222
223 if (cacheEntry == null) {
224 // 2. If a cached value isn't available, try fetching the operation.
225 // fetchAndSaveQuery will update the cache with either a Promise or
226 // an Error to throw, or a FragmentResource to return.
227 cacheEntry = this._fetchAndSaveQuery(cacheKey, operation, fetchObservable, fetchPolicy, renderPolicy, (0, _objectSpread2["default"])({}, observer, {
228 unsubscribe: function unsubscribe(subscription) {
229 // 4. If the request is cancelled, make sure to dispose
230 // of the temporary retain; this will ensure that a promise
231 // doesn't remain unnecessarilly cached until the temporary retain
232 // expires. Not clearing the temporary retain might cause the
233 // query to incorrectly re-suspend.
234 if (temporaryRetainDisposable != null) {
235 temporaryRetainDisposable.dispose();
236 }
237
238 var observerUnsubscribe = observer === null || observer === void 0 ? void 0 : observer.unsubscribe;
239 observerUnsubscribe && observerUnsubscribe(subscription);
240 }
241 }));
242 } // 3. Temporarily retain here in render phase. When the Component reading
243 // the operation is committed, we will transfer ownership of data retention
244 // to the component.
245 // In case the component never commits (mounts or updates) from this render,
246 // this data retention hold will auto-release itself afer a timeout.
247
248
249 temporaryRetainDisposable = cacheEntry.temporaryRetain(environment);
250 var cachedValue = cacheEntry.getValue();
251
252 if (isPromise(cachedValue) || cachedValue instanceof Error) {
253 throw cachedValue;
254 }
255
256 return cachedValue;
257 }
258 /**
259 * This function should be called during a Component's commit phase
260 * (e.g. inside useEffect), in order to retain the operation in the Relay store
261 * and transfer ownership of the operation to the component lifecycle.
262 */
263 ;
264
265 _proto.retain = function retain(queryResult) {
266 var environment = this._environment;
267 var cacheKey = queryResult.cacheKey,
268 operation = queryResult.operation;
269
270 var cacheEntry = this._getOrCreateCacheEntry(cacheKey, operation, queryResult, null);
271
272 var disposable = cacheEntry.permanentRetain(environment);
273 return {
274 dispose: function dispose() {
275 disposable.dispose();
276 }
277 };
278 };
279
280 _proto.getCacheEntry = function getCacheEntry(operation, fetchPolicy, maybeRenderPolicy) {
281 var _maybeRenderPolicy2;
282
283 var renderPolicy = (_maybeRenderPolicy2 = maybeRenderPolicy) !== null && _maybeRenderPolicy2 !== void 0 ? _maybeRenderPolicy2 : DEFAULT_RENDER_POLICY;
284 var cacheKey = getQueryCacheKey(operation, fetchPolicy, renderPolicy);
285 return this._cache.get(cacheKey);
286 };
287
288 _proto._getOrCreateCacheEntry = function _getOrCreateCacheEntry(cacheKey, operation, value, networkSubscription) {
289 var cacheEntry = this._cache.get(cacheKey);
290
291 if (cacheEntry == null) {
292 cacheEntry = createCacheEntry(cacheKey, operation, value, networkSubscription, this._clearCacheEntry);
293
294 this._cache.set(cacheKey, cacheEntry);
295 }
296
297 return cacheEntry;
298 };
299
300 _proto._fetchAndSaveQuery = function _fetchAndSaveQuery(cacheKey, operation, fetchObservable, fetchPolicy, renderPolicy, observer) {
301 var _this2 = this;
302
303 var environment = this._environment; // NOTE: Running `check` will write missing data to the store using any
304 // missing data handlers specified on the environment;
305 // We run it here first to make the handlers get a chance to populate
306 // missing data.
307
308 var hasFullQuery = environment.check(operation.root);
309 var canPartialRender = hasFullQuery || renderPolicy === 'partial';
310 var shouldFetch;
311 var shouldAllowRender;
312
313 var resolveNetworkPromise = function resolveNetworkPromise() {};
314
315 switch (fetchPolicy) {
316 case 'store-only':
317 {
318 shouldFetch = false;
319 shouldAllowRender = true;
320 break;
321 }
322
323 case 'store-or-network':
324 {
325 shouldFetch = !hasFullQuery;
326 shouldAllowRender = canPartialRender;
327 break;
328 }
329
330 case 'store-and-network':
331 {
332 shouldFetch = true;
333 shouldAllowRender = canPartialRender;
334 break;
335 }
336
337 case 'network-only':
338 default:
339 {
340 shouldFetch = true;
341 shouldAllowRender = false;
342 break;
343 }
344 } // NOTE: If this value is false, we will cache a promise for this
345 // query, which means we will suspend here at this query root.
346 // If it's true, we will cache the query resource and allow rendering to
347 // continue.
348
349
350 if (shouldAllowRender) {
351 var queryResult = getQueryResult(operation, cacheKey);
352
353 var _cacheEntry = createCacheEntry(cacheKey, operation, queryResult, null, this._clearCacheEntry);
354
355 this._cache.set(cacheKey, _cacheEntry);
356 }
357
358 environment.__log({
359 name: 'queryresource.fetch',
360 operation: operation,
361 fetchPolicy: fetchPolicy,
362 renderPolicy: renderPolicy,
363 hasFullQuery: hasFullQuery,
364 shouldFetch: shouldFetch
365 });
366
367 if (shouldFetch) {
368 var _queryResult = getQueryResult(operation, cacheKey);
369
370 var networkSubscription;
371 fetchObservable.subscribe({
372 start: function start(subscription) {
373 networkSubscription = subscription;
374
375 var cacheEntry = _this2._cache.get(cacheKey);
376
377 if (cacheEntry) {
378 cacheEntry.setNetworkSubscription(networkSubscription);
379 }
380
381 var observerStart = observer === null || observer === void 0 ? void 0 : observer.start;
382 observerStart && observerStart(subscription);
383 },
384 next: function next() {
385 var snapshot = environment.lookup(operation.fragment);
386
387 var cacheEntry = _this2._getOrCreateCacheEntry(cacheKey, operation, _queryResult, networkSubscription);
388
389 cacheEntry.setValue(_queryResult);
390 resolveNetworkPromise();
391 var observerNext = observer === null || observer === void 0 ? void 0 : observer.next;
392 observerNext && observerNext(snapshot);
393 },
394 error: function error(_error) {
395 var cacheEntry = _this2._getOrCreateCacheEntry(cacheKey, operation, _error, networkSubscription);
396
397 cacheEntry.setValue(_error);
398 resolveNetworkPromise();
399 networkSubscription = null;
400 cacheEntry.setNetworkSubscription(null);
401 var observerError = observer === null || observer === void 0 ? void 0 : observer.error;
402 observerError && observerError(_error);
403 },
404 complete: function complete() {
405 resolveNetworkPromise();
406 networkSubscription = null;
407
408 var cacheEntry = _this2._cache.get(cacheKey);
409
410 if (cacheEntry) {
411 cacheEntry.setNetworkSubscription(null);
412 }
413
414 var observerComplete = observer === null || observer === void 0 ? void 0 : observer.complete;
415 observerComplete && observerComplete();
416 },
417 unsubscribe: observer === null || observer === void 0 ? void 0 : observer.unsubscribe
418 });
419
420 var _cacheEntry2 = this._cache.get(cacheKey);
421
422 if (!_cacheEntry2) {
423 var networkPromise = new Promise(function (resolve) {
424 resolveNetworkPromise = resolve;
425 }); // $FlowExpectedError Expando to annotate Promises.
426
427 networkPromise.displayName = 'Relay(' + operation.fragment.node.name + ')';
428 _cacheEntry2 = createCacheEntry(cacheKey, operation, networkPromise, networkSubscription, this._clearCacheEntry);
429
430 this._cache.set(cacheKey, _cacheEntry2);
431 }
432 } else {
433 var observerComplete = observer === null || observer === void 0 ? void 0 : observer.complete;
434 observerComplete && observerComplete();
435 }
436
437 var cacheEntry = this._cache.get(cacheKey);
438
439 !(cacheEntry != null) ? process.env.NODE_ENV !== "production" ? invariant(false, 'Relay: Expected to have cached a result when attempting to fetch query.' + "If you're seeing this, this is likely a bug in Relay.") : invariant(false) : void 0;
440 return cacheEntry;
441 };
442
443 return QueryResourceImpl;
444}();
445
446function createQueryResource(environment) {
447 return new QueryResourceImpl(environment);
448}
449
450var dataResources = new Map();
451
452function getQueryResourceForEnvironment(environment) {
453 var cached = dataResources.get(environment);
454
455 if (cached) {
456 return cached;
457 }
458
459 var newDataResource = createQueryResource(environment);
460 dataResources.set(environment, newDataResource);
461 return newDataResource;
462}
463
464module.exports = {
465 createQueryResource: createQueryResource,
466 getQueryResourceForEnvironment: getQueryResourceForEnvironment
467};
\No newline at end of file