UNPKG

14.4 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 _toConsumableArray2 = _interopRequireDefault(require("@babel/runtime/helpers/toConsumableArray"));
18
19var LRUCache = require('./LRUCache');
20
21var invariant = require("fbjs/lib/invariant");
22
23var mapObject = require("fbjs/lib/mapObject");
24
25var warning = require("fbjs/lib/warning");
26
27var _require = require('relay-runtime'),
28 getPromiseForRequestInFlight = _require.__internal.getPromiseForRequestInFlight,
29 getFragmentIdentifier = _require.getFragmentIdentifier,
30 getSelector = _require.getSelector,
31 isPromise = _require.isPromise,
32 recycleNodesInto = _require.recycleNodesInto;
33
34// TODO: Fix to not rely on LRU. If the number of active fragments exceeds this
35// capacity, readSpec() will fail to find cached entries and break object
36// identity even if data hasn't changed.
37var CACHE_CAPACITY = 1000000;
38
39function isMissingData(snapshot) {
40 if (Array.isArray(snapshot)) {
41 return snapshot.some(function (s) {
42 return s.isMissingData;
43 });
44 }
45
46 return snapshot.isMissingData;
47}
48
49function getFragmentResult(cacheKey, snapshot) {
50 if (Array.isArray(snapshot)) {
51 return {
52 cacheKey: cacheKey,
53 snapshot: snapshot,
54 data: snapshot.map(function (s) {
55 return s.data;
56 })
57 };
58 }
59
60 return {
61 cacheKey: cacheKey,
62 snapshot: snapshot,
63 data: snapshot.data
64 };
65}
66
67function getPromiseForPendingOperationAffectingOwner(environment, request) {
68 return environment.getOperationTracker().getPromiseForPendingOperationsAffectingOwner(request);
69}
70
71var FragmentResourceImpl =
72/*#__PURE__*/
73function () {
74 function FragmentResourceImpl(environment) {
75 this._environment = environment;
76 this._cache = LRUCache.create(CACHE_CAPACITY);
77 }
78 /**
79 * This function should be called during a Component's render function,
80 * to read the data for a fragment, or suspend if the fragment is being
81 * fetched.
82 */
83
84
85 var _proto = FragmentResourceImpl.prototype;
86
87 _proto.read = function read(fragmentNode, fragmentRef, componentDisplayName, fragmentKey) {
88 return this.readWithIdentifier(fragmentNode, fragmentRef, getFragmentIdentifier(fragmentNode, fragmentRef), componentDisplayName, fragmentKey);
89 }
90 /**
91 * Like `read`, but with pre-computed fragmentIdentifier that should be
92 * equal to `getFragmentIdentifier(fragmentNode, fragmentRef)` from the
93 * arguments.
94 */
95 ;
96
97 _proto.readWithIdentifier = function readWithIdentifier(fragmentNode, fragmentRef, fragmentIdentifier, componentDisplayName, fragmentKey) {
98 var _fragmentNode$metadat, _fragmentOwner$node$p;
99
100 var environment = this._environment; // If fragmentRef is null or undefined, pass it directly through.
101 // This is a convenience when consuming fragments via a HOC api, when the
102 // prop corresponding to the fragment ref might be passed as null.
103
104 if (fragmentRef == null) {
105 return {
106 cacheKey: fragmentIdentifier,
107 data: null,
108 snapshot: null
109 };
110 } // If fragmentRef is plural, ensure that it is an array.
111 // If it's empty, return the empty array direclty before doing any more work.
112
113
114 if ((fragmentNode === null || fragmentNode === void 0 ? void 0 : (_fragmentNode$metadat = fragmentNode.metadata) === null || _fragmentNode$metadat === void 0 ? void 0 : _fragmentNode$metadat.plural) === true) {
115 !Array.isArray(fragmentRef) ? process.env.NODE_ENV !== "production" ? invariant(false, 'Relay: Expected fragment pointer%s for fragment `%s` to be ' + 'an array, instead got `%s`. Remove `@relay(plural: true)` ' + 'from fragment `%s` to allow the prop to be an object.', fragmentKey != null ? " for key `".concat(fragmentKey, "`") : '', fragmentNode.name, typeof fragmentRef, fragmentNode.name) : invariant(false) : void 0;
116
117 if (fragmentRef.length === 0) {
118 return {
119 cacheKey: fragmentIdentifier,
120 data: [],
121 snapshot: []
122 };
123 }
124 } // Now we actually attempt to read the fragment:
125 // 1. Check if there's a cached value for this fragment
126
127
128 var cachedValue = this._cache.get(fragmentIdentifier);
129
130 if (cachedValue != null) {
131 if (isPromise(cachedValue) || cachedValue instanceof Error) {
132 throw cachedValue;
133 }
134
135 return getFragmentResult(fragmentIdentifier, cachedValue);
136 } // 2. If not, try reading the fragment from the Relay store.
137 // If the snapshot has data, return it and save it in cache
138
139
140 var fragmentSelector = getSelector(fragmentNode, fragmentRef);
141 !(fragmentSelector != null) ? process.env.NODE_ENV !== "production" ? invariant(false, 'Relay: Expected to have received a valid ' + 'fragment reference for fragment `%s` declared in `%s`. Make sure ' + "that `%s`'s parent is passing the right fragment reference prop.", fragmentNode.name, componentDisplayName, componentDisplayName) : invariant(false) : void 0;
142 var snapshot = fragmentSelector.kind === 'PluralReaderSelector' ? fragmentSelector.selectors.map(function (s) {
143 return environment.lookup(s);
144 }) : environment.lookup(fragmentSelector);
145 var fragmentOwner = fragmentSelector.kind === 'PluralReaderSelector' ? fragmentSelector.selectors[0].owner : fragmentSelector.owner;
146 var parentQueryName = (_fragmentOwner$node$p = fragmentOwner.node.params.name) !== null && _fragmentOwner$node$p !== void 0 ? _fragmentOwner$node$p : 'Unknown Parent Query';
147
148 if (!isMissingData(snapshot)) {
149 this._cache.set(fragmentIdentifier, snapshot);
150
151 return getFragmentResult(fragmentIdentifier, snapshot);
152 } // 3. If we don't have data in the store, check if a request is in
153 // flight for the fragment's parent query, or for another operation
154 // that may affect the parent's query data, such as a mutation
155 // or subscription. If a promise exists, cache the promise and use it
156 // to suspend.
157
158
159 var networkPromise = this._getAndSavePromiseForFragmentRequestInFlight(fragmentIdentifier, fragmentOwner);
160
161 if (networkPromise != null) {
162 throw networkPromise;
163 } // 5. If a cached value still isn't available, raise a warning.
164 // This means that we're trying to read a fragment that isn't available
165 // and isn't being fetched at all.
166
167
168 process.env.NODE_ENV !== "production" ? warning(false, 'Relay: Tried reading fragment `%s` declared in ' + '`%s`, but it has missing data and its parent query `%s` is not ' + 'being fetched.\n' + 'This might be fixed by by re-running the Relay Compiler. ' + ' Otherwise, make sure of the following:\n' + '* You are correctly fetching `%s` if you are using a ' + '"store-only" `fetchPolicy`.\n' + "* Other queries aren't accidentally fetching and overwriting " + 'the data for this fragment.\n' + '* Any related mutations or subscriptions are fetching all of ' + 'the data for this fragment.\n' + "* Any related store updaters aren't accidentally deleting " + 'data for this fragment.', fragmentNode.name, componentDisplayName, parentQueryName, parentQueryName) : void 0;
169 return getFragmentResult(fragmentIdentifier, snapshot);
170 };
171
172 _proto.readSpec = function readSpec(fragmentNodes, fragmentRefs, componentDisplayName) {
173 var _this = this;
174
175 return mapObject(fragmentNodes, function (fragmentNode, fragmentKey) {
176 var fragmentRef = fragmentRefs[fragmentKey];
177 return _this.read(fragmentNode, fragmentRef, componentDisplayName, fragmentKey);
178 });
179 };
180
181 _proto.subscribe = function subscribe(fragmentResult, callback) {
182 var _this2 = this;
183
184 var environment = this._environment;
185 var cacheKey = fragmentResult.cacheKey;
186 var renderedSnapshot = fragmentResult.snapshot;
187
188 if (!renderedSnapshot) {
189 return {
190 dispose: function dispose() {}
191 };
192 } // 1. Check for any updates missed during render phase
193 // TODO(T44066760): More efficiently detect if we missed an update
194
195
196 var _this$checkMissedUpda = this.checkMissedUpdates(fragmentResult),
197 didMissUpdates = _this$checkMissedUpda[0],
198 currentSnapshot = _this$checkMissedUpda[1]; // 2. If an update was missed, notify the component so it updates with
199 // latest data.
200
201
202 if (didMissUpdates) {
203 callback();
204 } // 3. Establish subscriptions on the snapshot(s)
205
206
207 var dataSubscriptions = [];
208
209 if (Array.isArray(renderedSnapshot)) {
210 !Array.isArray(currentSnapshot) ? process.env.NODE_ENV !== "production" ? invariant(false, 'Relay: Expected snapshots to be plural. ' + "If you're seeing this, this is likely a bug in Relay.") : invariant(false) : void 0;
211 currentSnapshot.forEach(function (snapshot, idx) {
212 dataSubscriptions.push(environment.subscribe(snapshot, function (latestSnapshot) {
213 _this2._updatePluralSnapshot(cacheKey, latestSnapshot, idx);
214
215 callback();
216 }));
217 });
218 } else {
219 !(currentSnapshot != null && !Array.isArray(currentSnapshot)) ? process.env.NODE_ENV !== "production" ? invariant(false, 'Relay: Expected snapshot to be singular. ' + "If you're seeing this, this is likely a bug in Relay.") : invariant(false) : void 0;
220 dataSubscriptions.push(environment.subscribe(currentSnapshot, function (latestSnapshot) {
221 _this2._cache.set(cacheKey, latestSnapshot);
222
223 callback();
224 }));
225 }
226
227 return {
228 dispose: function dispose() {
229 dataSubscriptions.map(function (s) {
230 return s.dispose();
231 });
232
233 _this2._cache["delete"](cacheKey);
234 }
235 };
236 };
237
238 _proto.subscribeSpec = function subscribeSpec(fragmentResults, callback) {
239 var _this3 = this;
240
241 var disposables = Object.keys(fragmentResults).map(function (key) {
242 return _this3.subscribe(fragmentResults[key], callback);
243 });
244 return {
245 dispose: function dispose() {
246 disposables.forEach(function (disposable) {
247 disposable.dispose();
248 });
249 }
250 };
251 };
252
253 _proto.checkMissedUpdates = function checkMissedUpdates(fragmentResult) {
254 var environment = this._environment;
255 var cacheKey = fragmentResult.cacheKey;
256 var renderedSnapshot = fragmentResult.snapshot;
257
258 if (!renderedSnapshot) {
259 return [false, null];
260 }
261
262 var didMissUpdates = false;
263
264 if (Array.isArray(renderedSnapshot)) {
265 var currentSnapshots = [];
266 renderedSnapshot.forEach(function (snapshot, idx) {
267 var currentSnapshot = environment.lookup(snapshot.selector);
268 var renderData = snapshot.data;
269 var currentData = currentSnapshot.data;
270 var updatedData = recycleNodesInto(renderData, currentData);
271
272 if (updatedData !== renderData) {
273 currentSnapshot = (0, _objectSpread2["default"])({}, currentSnapshot, {
274 data: updatedData
275 });
276 didMissUpdates = true;
277 }
278
279 currentSnapshots[idx] = currentSnapshot;
280 });
281
282 if (didMissUpdates) {
283 this._cache.set(cacheKey, currentSnapshots);
284 }
285
286 return [didMissUpdates, currentSnapshots];
287 }
288
289 var currentSnapshot = environment.lookup(renderedSnapshot.selector);
290 var renderData = renderedSnapshot.data;
291 var currentData = currentSnapshot.data;
292 var updatedData = recycleNodesInto(renderData, currentData);
293
294 if (updatedData !== renderData) {
295 currentSnapshot = (0, _objectSpread2["default"])({}, currentSnapshot, {
296 data: updatedData
297 });
298
299 this._cache.set(cacheKey, currentSnapshot);
300
301 didMissUpdates = true;
302 }
303
304 return [didMissUpdates, currentSnapshot];
305 };
306
307 _proto.checkMissedUpdatesSpec = function checkMissedUpdatesSpec(fragmentResults) {
308 var _this4 = this;
309
310 return Object.keys(fragmentResults).some(function (key) {
311 return _this4.checkMissedUpdates(fragmentResults[key])[0];
312 });
313 };
314
315 _proto._getAndSavePromiseForFragmentRequestInFlight = function _getAndSavePromiseForFragmentRequestInFlight(cacheKey, fragmentOwner) {
316 var _this5 = this;
317
318 var _getPromiseForRequest;
319
320 var environment = this._environment;
321 var networkPromise = (_getPromiseForRequest = getPromiseForRequestInFlight(environment, fragmentOwner)) !== null && _getPromiseForRequest !== void 0 ? _getPromiseForRequest : getPromiseForPendingOperationAffectingOwner(environment, fragmentOwner);
322
323 if (!networkPromise) {
324 return null;
325 } // When the Promise for the request resolves, we need to make sure to
326 // update the cache with the latest data available in the store before
327 // resolving the Promise
328
329
330 var promise = networkPromise.then(function () {
331 _this5._cache["delete"](cacheKey);
332 })["catch"](function (error) {
333 _this5._cache.set(cacheKey, error);
334 });
335
336 this._cache.set(cacheKey, promise); // $FlowExpectedError Expando to annotate Promises.
337
338
339 promise.displayName = 'Relay(' + fragmentOwner.node.params.name + ')';
340 return promise;
341 };
342
343 _proto._updatePluralSnapshot = function _updatePluralSnapshot(cacheKey, latestSnapshot, idx) {
344 var currentSnapshots = this._cache.get(cacheKey);
345
346 !Array.isArray(currentSnapshots) ? process.env.NODE_ENV !== "production" ? invariant(false, 'Relay: Expected to find cached data for plural fragment when ' + 'recieving a subscription. ' + "If you're seeing this, this is likely a bug in Relay.") : invariant(false) : void 0;
347 var nextSnapshots = (0, _toConsumableArray2["default"])(currentSnapshots);
348 nextSnapshots[idx] = latestSnapshot;
349
350 this._cache.set(cacheKey, nextSnapshots);
351 };
352
353 return FragmentResourceImpl;
354}();
355
356function createFragmentResource(environment) {
357 return new FragmentResourceImpl(environment);
358}
359
360var dataResources = new Map();
361
362function getFragmentResourceForEnvironment(environment) {
363 var cached = dataResources.get(environment);
364
365 if (cached) {
366 return cached;
367 }
368
369 var newDataResource = createFragmentResource(environment);
370 dataResources.set(environment, newDataResource);
371 return newDataResource;
372}
373
374module.exports = {
375 createFragmentResource: createFragmentResource,
376 getFragmentResourceForEnvironment: getFragmentResourceForEnvironment
377};
\No newline at end of file