UNPKG

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