react-relay 11.0.1 → 13.0.0-rc.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (150) hide show
  1. package/ReactRelayContext.js +1 -1
  2. package/ReactRelayContext.js.flow +2 -3
  3. package/ReactRelayFragmentContainer.js.flow +24 -24
  4. package/ReactRelayFragmentMockRenderer.js.flow +1 -1
  5. package/ReactRelayLocalQueryRenderer.js.flow +6 -7
  6. package/ReactRelayPaginationContainer.js.flow +111 -58
  7. package/ReactRelayQueryFetcher.js.flow +9 -10
  8. package/ReactRelayQueryRenderer.js.flow +115 -81
  9. package/ReactRelayRefetchContainer.js.flow +41 -38
  10. package/ReactRelayTestMocker.js.flow +16 -14
  11. package/ReactRelayTypes.js.flow +10 -10
  12. package/RelayContext.js.flow +3 -3
  13. package/__flowtests__/ReactRelayFragmentContainer-flowtest.js.flow +1 -2
  14. package/__flowtests__/ReactRelayPaginationContainer-flowtest.js.flow +11 -7
  15. package/__flowtests__/ReactRelayRefetchContainer-flowtest.js.flow +10 -6
  16. package/__flowtests__/RelayModern-flowtest.js.flow +78 -46
  17. package/__flowtests__/RelayModernFlowtest_badref.graphql.js.flow +5 -4
  18. package/__flowtests__/RelayModernFlowtest_notref.graphql.js.flow +5 -4
  19. package/__flowtests__/RelayModernFlowtest_user.graphql.js.flow +4 -3
  20. package/__flowtests__/RelayModernFlowtest_users.graphql.js.flow +4 -3
  21. package/__flowtests__/__generated__/ReactRelayFragmentContainerFlowtest_viewer.graphql.js.flow +72 -0
  22. package/__flowtests__/__generated__/ReactRelayFragmentContainerFlowtest_viewer2.graphql.js.flow +72 -0
  23. package/__flowtests__/__generated__/ReactRelayPaginationContainerFlowtestQuery.graphql.js.flow +227 -0
  24. package/__flowtests__/__generated__/ReactRelayPaginationContainerFlowtest_viewer.graphql.js.flow +164 -0
  25. package/__flowtests__/__generated__/ReactRelayRefetchContainerFlowtestQuery.graphql.js.flow +227 -0
  26. package/__flowtests__/__generated__/ReactRelayRefetchContainerFlowtest_viewer.graphql.js.flow +164 -0
  27. package/__flowtests__/__generated__/RelayModernFlowtest_badref.graphql.js.flow +66 -0
  28. package/__flowtests__/__generated__/RelayModernFlowtest_notref.graphql.js.flow +66 -0
  29. package/__flowtests__/__generated__/RelayModernFlowtest_user.graphql.js.flow +59 -0
  30. package/__flowtests__/__generated__/RelayModernFlowtest_users.graphql.js.flow +61 -0
  31. package/assertFragmentMap.js.flow +2 -2
  32. package/buildReactRelayContainer.js.flow +15 -12
  33. package/getRootVariablesForFragments.js.flow +2 -4
  34. package/hooks.js +1 -1
  35. package/hooks.js.flow +5 -6
  36. package/index.js +1 -1
  37. package/index.js.flow +6 -7
  38. package/jest-react/enqueueTask.js.flow +56 -0
  39. package/jest-react/index.js.flow +12 -0
  40. package/jest-react/internalAct.js.flow +139 -0
  41. package/legacy.js +1 -1
  42. package/lib/ReactRelayFragmentContainer.js +21 -15
  43. package/lib/ReactRelayFragmentMockRenderer.js +2 -2
  44. package/lib/ReactRelayLocalQueryRenderer.js +7 -8
  45. package/lib/ReactRelayPaginationContainer.js +96 -38
  46. package/lib/ReactRelayQueryFetcher.js +3 -3
  47. package/lib/ReactRelayQueryRenderer.js +86 -53
  48. package/lib/ReactRelayRefetchContainer.js +38 -25
  49. package/lib/ReactRelayTestMocker.js +8 -9
  50. package/lib/RelayContext.js +3 -2
  51. package/lib/assertFragmentMap.js +3 -2
  52. package/lib/buildReactRelayContainer.js +14 -11
  53. package/lib/getRootVariablesForFragments.js +1 -2
  54. package/lib/hooks.js +5 -5
  55. package/lib/index.js +7 -7
  56. package/lib/jest-react/enqueueTask.js +53 -0
  57. package/lib/jest-react/index.js +13 -0
  58. package/lib/jest-react/internalAct.js +116 -0
  59. package/lib/multi-actor/ActorChange.js +30 -0
  60. package/lib/multi-actor/index.js +11 -0
  61. package/lib/multi-actor/useRelayActorEnvironment.js +29 -0
  62. package/lib/relay-hooks/EntryPointContainer.react.js +3 -3
  63. package/lib/relay-hooks/FragmentResource.js +347 -92
  64. package/lib/relay-hooks/LRUCache.js +1 -1
  65. package/lib/relay-hooks/LazyLoadEntryPointContainer_DEPRECATED.react.js +4 -4
  66. package/lib/relay-hooks/MatchContainer.js +1 -1
  67. package/lib/relay-hooks/QueryResource.js +172 -29
  68. package/lib/relay-hooks/RelayEnvironmentProvider.js +5 -3
  69. package/lib/relay-hooks/SuspenseResource.js +130 -0
  70. package/lib/relay-hooks/loadQuery.js +42 -20
  71. package/lib/relay-hooks/preloadQuery_DEPRECATED.js +24 -15
  72. package/lib/relay-hooks/useBlockingPaginationFragment.js +4 -5
  73. package/lib/relay-hooks/useEntryPointLoader.js +2 -2
  74. package/lib/relay-hooks/useFetchTrackingRef.js +2 -1
  75. package/lib/relay-hooks/useFragment.js +8 -7
  76. package/lib/relay-hooks/useFragmentNode.js +4 -4
  77. package/lib/relay-hooks/useIsOperationNodeActive.js +3 -3
  78. package/lib/relay-hooks/useLazyLoadQuery.js +3 -3
  79. package/lib/relay-hooks/useLazyLoadQueryNode.js +10 -4
  80. package/lib/relay-hooks/useLoadMoreFunction.js +8 -12
  81. package/lib/relay-hooks/useMemoOperationDescriptor.js +2 -2
  82. package/lib/relay-hooks/useMemoVariables.js +2 -2
  83. package/lib/relay-hooks/useMutation.js +17 -6
  84. package/lib/relay-hooks/usePaginationFragment.js +2 -3
  85. package/lib/relay-hooks/usePreloadedQuery.js +8 -7
  86. package/lib/relay-hooks/useQueryLoader.js +30 -10
  87. package/lib/relay-hooks/useRefetchableFragmentNode.js +13 -17
  88. package/lib/relay-hooks/useRelayEnvironment.js +3 -3
  89. package/lib/relay-hooks/useStaticFragmentNodeWarning.js +2 -2
  90. package/lib/relay-hooks/useSubscribeToInvalidationState.js +2 -1
  91. package/lib/relay-hooks/useSubscription.js +10 -7
  92. package/multi-actor/ActorChange.js.flow +58 -0
  93. package/multi-actor/index.js.flow +14 -0
  94. package/multi-actor/useRelayActorEnvironment.js.flow +49 -0
  95. package/package.json +3 -2
  96. package/react-relay-hooks.js +2 -2
  97. package/react-relay-hooks.min.js +2 -2
  98. package/react-relay-legacy.js +2 -2
  99. package/react-relay-legacy.min.js +2 -2
  100. package/react-relay.js +2 -2
  101. package/react-relay.min.js +2 -2
  102. package/relay-hooks/EntryPointContainer.react.js.flow +8 -15
  103. package/relay-hooks/EntryPointTypes.flow.js.flow +24 -25
  104. package/relay-hooks/FragmentResource.js.flow +368 -94
  105. package/relay-hooks/LazyLoadEntryPointContainer_DEPRECATED.react.js.flow +32 -46
  106. package/relay-hooks/MatchContainer.js.flow +3 -2
  107. package/relay-hooks/QueryResource.js.flow +216 -25
  108. package/relay-hooks/RelayEnvironmentProvider.js.flow +14 -4
  109. package/relay-hooks/SuspenseResource.js.flow +115 -0
  110. package/relay-hooks/__flowtests__/EntryPointTypes/EntryPointElementConfig-flowtest.js.flow +4 -3
  111. package/relay-hooks/__flowtests__/EntryPointTypes/NestedEntrypoints-flowtest.js.flow +1 -1
  112. package/relay-hooks/__flowtests__/useBlockingPaginationFragment-flowtest.js.flow +10 -9
  113. package/relay-hooks/__flowtests__/useFragment-flowtest.js.flow +8 -7
  114. package/relay-hooks/__flowtests__/usePaginationFragment-flowtest.js.flow +10 -9
  115. package/relay-hooks/__flowtests__/useRefetchableFragment-flowtest.js.flow +10 -9
  116. package/relay-hooks/__flowtests__/utils.js.flow +8 -12
  117. package/relay-hooks/loadEntryPoint.js.flow +6 -12
  118. package/relay-hooks/loadQuery.js.flow +49 -31
  119. package/relay-hooks/preloadQuery_DEPRECATED.js.flow +30 -21
  120. package/relay-hooks/prepareEntryPoint_DEPRECATED.js.flow +6 -12
  121. package/relay-hooks/useBlockingPaginationFragment.js.flow +13 -11
  122. package/relay-hooks/useEntryPointLoader.js.flow +7 -10
  123. package/relay-hooks/useFetchTrackingRef.js.flow +2 -2
  124. package/relay-hooks/useFragment.js.flow +26 -46
  125. package/relay-hooks/useFragmentNode.js.flow +5 -7
  126. package/relay-hooks/useIsOperationNodeActive.js.flow +3 -5
  127. package/relay-hooks/useIsParentQueryActive.js.flow +3 -4
  128. package/relay-hooks/useLazyLoadQuery.js.flow +9 -10
  129. package/relay-hooks/useLazyLoadQueryNode.js.flow +19 -13
  130. package/relay-hooks/useLoadMoreFunction.js.flow +20 -29
  131. package/relay-hooks/useMemoOperationDescriptor.js.flow +5 -7
  132. package/relay-hooks/useMemoVariables.js.flow +6 -6
  133. package/relay-hooks/useMutation.js.flow +26 -26
  134. package/relay-hooks/usePaginationFragment.js.flow +38 -44
  135. package/relay-hooks/usePreloadedQuery.js.flow +18 -14
  136. package/relay-hooks/useQueryLoader.js.flow +41 -22
  137. package/relay-hooks/useRefetchableFragment.js.flow +7 -8
  138. package/relay-hooks/useRefetchableFragmentNode.js.flow +24 -32
  139. package/relay-hooks/useRelayEnvironment.js.flow +2 -4
  140. package/relay-hooks/useStaticFragmentNodeWarning.js.flow +2 -3
  141. package/relay-hooks/useSubscribeToInvalidationState.js.flow +3 -6
  142. package/relay-hooks/useSubscription.js.flow +20 -10
  143. package/lib/relay-hooks/getPaginationMetadata.js +0 -41
  144. package/lib/relay-hooks/getPaginationVariables.js +0 -67
  145. package/lib/relay-hooks/getRefetchMetadata.js +0 -36
  146. package/lib/relay-hooks/getValueAtPath.js +0 -51
  147. package/relay-hooks/getPaginationMetadata.js.flow +0 -74
  148. package/relay-hooks/getPaginationVariables.js.flow +0 -110
  149. package/relay-hooks/getRefetchMetadata.js.flow +0 -80
  150. package/relay-hooks/getValueAtPath.js.flow +0 -46
@@ -13,32 +13,46 @@
13
13
 
14
14
  'use strict';
15
15
 
16
- const LRUCache = require('./LRUCache');
16
+ import type {Cache} from './LRUCache';
17
+ import type {QueryResource, QueryResult} from './QueryResource';
18
+ import type {
19
+ ConcreteRequest,
20
+ DataID,
21
+ Disposable,
22
+ IEnvironment,
23
+ ReaderFragment,
24
+ RequestDescriptor,
25
+ Snapshot,
26
+ } from 'relay-runtime';
17
27
 
28
+ const LRUCache = require('./LRUCache');
29
+ const {getQueryResourceForEnvironment} = require('./QueryResource');
30
+ const SuspenseResource = require('./SuspenseResource');
18
31
  const invariant = require('invariant');
19
- const mapObject = require('mapObject');
20
-
21
32
  const {
22
- __internal: {getPromiseForActiveRequest},
33
+ RelayFeatureFlags,
34
+ __internal: {fetchQuery, getPromiseForActiveRequest},
35
+ createOperationDescriptor,
23
36
  getFragmentIdentifier,
37
+ getPendingOperationsForFragment,
24
38
  getSelector,
39
+ getVariablesFromFragment,
25
40
  isPromise,
26
41
  recycleNodesInto,
27
42
  reportMissingRequiredFields,
28
43
  } = require('relay-runtime');
29
44
 
30
- import type {Cache} from './LRUCache';
31
- import type {
32
- Disposable,
33
- IEnvironment,
34
- ReaderFragment,
35
- RequestDescriptor,
36
- Snapshot,
37
- } from 'relay-runtime';
38
-
39
45
  export type FragmentResource = FragmentResourceImpl;
40
46
 
41
- type FragmentResourceCache = Cache<Promise<mixed> | FragmentResult>;
47
+ type FragmentResourceCache = Cache<
48
+ | {|
49
+ kind: 'pending',
50
+ pendingOperations: $ReadOnlyArray<RequestDescriptor>,
51
+ promise: Promise<mixed>,
52
+ result: FragmentResult,
53
+ |}
54
+ | {|kind: 'done', result: FragmentResult|},
55
+ >;
42
56
 
43
57
  const WEAKMAP_SUPPORTED = typeof WeakMap === 'function';
44
58
  interface IMap<K, V> {
@@ -47,10 +61,13 @@ interface IMap<K, V> {
47
61
  }
48
62
 
49
63
  type SingularOrPluralSnapshot = Snapshot | $ReadOnlyArray<Snapshot>;
64
+
50
65
  opaque type FragmentResult: {data: mixed, ...} = {|
51
66
  cacheKey: string,
52
67
  data: mixed,
68
+ isMissingData: boolean,
53
69
  snapshot: SingularOrPluralSnapshot | null,
70
+ storeEpoch: number,
54
71
  |};
55
72
 
56
73
  // TODO: Fix to not rely on LRU. If the number of active fragments exceeds this
@@ -61,39 +78,123 @@ const CACHE_CAPACITY = 1000000;
61
78
  // this is frozen so that users don't accidentally push data into the array
62
79
  const CONSTANT_READONLY_EMPTY_ARRAY = Object.freeze([]);
63
80
 
64
- function isMissingData(snapshot: SingularOrPluralSnapshot) {
81
+ function isMissingData(snapshot: SingularOrPluralSnapshot): boolean {
65
82
  if (Array.isArray(snapshot)) {
66
83
  return snapshot.some(s => s.isMissingData);
67
84
  }
68
85
  return snapshot.isMissingData;
69
86
  }
70
87
 
88
+ function hasMissingClientEdges(snapshot: SingularOrPluralSnapshot): boolean {
89
+ if (Array.isArray(snapshot)) {
90
+ return snapshot.some(s => (s.missingClientEdges?.length ?? 0) > 0);
91
+ }
92
+ return (snapshot.missingClientEdges?.length ?? 0) > 0;
93
+ }
94
+
95
+ function singularOrPluralForEach(
96
+ snapshot: SingularOrPluralSnapshot,
97
+ f: Snapshot => void,
98
+ ): void {
99
+ if (Array.isArray(snapshot)) {
100
+ snapshot.forEach(f);
101
+ } else {
102
+ f(snapshot);
103
+ }
104
+ }
105
+
71
106
  function getFragmentResult(
72
107
  cacheKey: string,
73
108
  snapshot: SingularOrPluralSnapshot,
109
+ storeEpoch: number,
74
110
  ): FragmentResult {
75
111
  if (Array.isArray(snapshot)) {
76
- return {cacheKey, snapshot, data: snapshot.map(s => s.data)};
112
+ return {
113
+ cacheKey,
114
+ snapshot,
115
+ data: snapshot.map(s => s.data),
116
+ isMissingData: isMissingData(snapshot),
117
+ storeEpoch,
118
+ };
77
119
  }
78
- return {cacheKey, snapshot, data: snapshot.data};
120
+ return {
121
+ cacheKey,
122
+ snapshot,
123
+ data: snapshot.data,
124
+ isMissingData: isMissingData(snapshot),
125
+ storeEpoch,
126
+ };
79
127
  }
80
128
 
81
- function getPromiseForPendingOperationAffectingOwner(
82
- environment: IEnvironment,
83
- request: RequestDescriptor,
84
- ): Promise<void> | null {
85
- return environment
86
- .getOperationTracker()
87
- .getPromiseForPendingOperationsAffectingOwner(request);
129
+ /**
130
+ * The purpose of this cache is to allow information to be passed from an
131
+ * initial read which suspends through to the commit that follows a subsequent
132
+ * successful read. Specifically, the QueryResource result for the data fetch
133
+ * is passed through so that that query can be retained on commit.
134
+ */
135
+ class ClientEdgeQueryResultsCache {
136
+ _cache: Map<string, [Array<QueryResult>, SuspenseResource]> = new Map();
137
+ _retainCounts: Map<string, number> = new Map();
138
+ _environment: IEnvironment;
139
+
140
+ constructor(environment: IEnvironment) {
141
+ this._environment = environment;
142
+ }
143
+
144
+ get(fragmentIdentifier: string): void | Array<QueryResult> {
145
+ return this._cache.get(fragmentIdentifier)?.[0] ?? undefined;
146
+ }
147
+
148
+ recordQueryResults(
149
+ fragmentIdentifier: string,
150
+ value: Array<QueryResult>, // may be mutated after being passed here
151
+ ): void {
152
+ const existing = this._cache.get(fragmentIdentifier);
153
+ if (!existing) {
154
+ const suspenseResource = new SuspenseResource(() =>
155
+ this._retain(fragmentIdentifier),
156
+ );
157
+ this._cache.set(fragmentIdentifier, [value, suspenseResource]);
158
+ suspenseResource.temporaryRetain(this._environment);
159
+ } else {
160
+ const [existingResults, suspenseResource] = existing;
161
+ value.forEach(queryResult => {
162
+ existingResults.push(queryResult);
163
+ });
164
+ suspenseResource.temporaryRetain(this._environment);
165
+ }
166
+ }
167
+
168
+ _retain(id) {
169
+ const retainCount = (this._retainCounts.get(id) ?? 0) + 1;
170
+ this._retainCounts.set(id, retainCount);
171
+ return {
172
+ dispose: () => {
173
+ const newRetainCount = (this._retainCounts.get(id) ?? 0) - 1;
174
+ if (newRetainCount > 0) {
175
+ this._retainCounts.set(id, newRetainCount);
176
+ } else {
177
+ this._retainCounts.delete(id);
178
+ this._cache.delete(id);
179
+ }
180
+ },
181
+ };
182
+ }
88
183
  }
89
184
 
90
185
  class FragmentResourceImpl {
91
186
  _environment: IEnvironment;
92
187
  _cache: FragmentResourceCache;
188
+ _clientEdgeQueryResultsCache: void | ClientEdgeQueryResultsCache;
93
189
 
94
190
  constructor(environment: IEnvironment) {
95
191
  this._environment = environment;
96
192
  this._cache = LRUCache.create(CACHE_CAPACITY);
193
+ if (RelayFeatureFlags.ENABLE_CLIENT_EDGES) {
194
+ this._clientEdgeQueryResultsCache = new ClientEdgeQueryResultsCache(
195
+ environment,
196
+ );
197
+ }
97
198
  }
98
199
 
99
200
  /**
@@ -134,9 +235,17 @@ class FragmentResourceImpl {
134
235
  // This is a convenience when consuming fragments via a HOC API, when the
135
236
  // prop corresponding to the fragment ref might be passed as null.
136
237
  if (fragmentRef == null) {
137
- return {cacheKey: fragmentIdentifier, data: null, snapshot: null};
238
+ return {
239
+ cacheKey: fragmentIdentifier,
240
+ data: null,
241
+ isMissingData: false,
242
+ snapshot: null,
243
+ storeEpoch: 0,
244
+ };
138
245
  }
139
246
 
247
+ const storeEpoch = environment.getStore().getEpoch();
248
+
140
249
  // If fragmentRef is plural, ensure that it is an array.
141
250
  // If it's empty, return the empty array directly before doing any more work.
142
251
  if (fragmentNode?.metadata?.plural === true) {
@@ -154,7 +263,9 @@ class FragmentResourceImpl {
154
263
  return {
155
264
  cacheKey: fragmentIdentifier,
156
265
  data: CONSTANT_READONLY_EMPTY_ARRAY,
266
+ isMissingData: false,
157
267
  snapshot: CONSTANT_READONLY_EMPTY_ARRAY,
268
+ storeEpoch,
158
269
  };
159
270
  }
160
271
  }
@@ -164,12 +275,24 @@ class FragmentResourceImpl {
164
275
  // 1. Check if there's a cached value for this fragment
165
276
  const cachedValue = this._cache.get(fragmentIdentifier);
166
277
  if (cachedValue != null) {
167
- if (isPromise(cachedValue)) {
168
- throw cachedValue;
278
+ if (cachedValue.kind === 'pending' && isPromise(cachedValue.promise)) {
279
+ environment.__log({
280
+ name: 'suspense.fragment',
281
+ data: cachedValue.result.data,
282
+ fragment: fragmentNode,
283
+ isRelayHooks: true,
284
+ isMissingData: cachedValue.result.isMissingData,
285
+ isPromiseCached: true,
286
+ pendingOperations: cachedValue.pendingOperations,
287
+ });
288
+ throw cachedValue.promise;
169
289
  }
170
- if (cachedValue.snapshot) {
171
- this._reportMissingRequiredFieldsInSnapshot(cachedValue.snapshot);
172
- return cachedValue;
290
+
291
+ if (cachedValue.kind === 'done' && cachedValue.result.snapshot) {
292
+ this._reportMissingRequiredFieldsInSnapshot(
293
+ cachedValue.result.snapshot,
294
+ );
295
+ return cachedValue.result;
173
296
  }
174
297
  }
175
298
 
@@ -201,33 +324,142 @@ class FragmentResourceImpl {
201
324
  ? fragmentSelector.selectors.map(s => environment.lookup(s))
202
325
  : environment.lookup(fragmentSelector);
203
326
 
204
- const fragmentOwner =
205
- fragmentSelector.kind === 'PluralReaderSelector'
206
- ? fragmentSelector.selectors[0].owner
207
- : fragmentSelector.owner;
208
-
209
- if (!isMissingData(snapshot)) {
327
+ const fragmentResult = getFragmentResult(
328
+ fragmentIdentifier,
329
+ snapshot,
330
+ storeEpoch,
331
+ );
332
+ if (!fragmentResult.isMissingData) {
210
333
  this._reportMissingRequiredFieldsInSnapshot(snapshot);
211
- const fragmentResult = getFragmentResult(fragmentIdentifier, snapshot);
212
- this._cache.set(fragmentIdentifier, fragmentResult);
334
+
335
+ this._cache.set(fragmentIdentifier, {
336
+ kind: 'done',
337
+ result: fragmentResult,
338
+ });
213
339
  return fragmentResult;
214
340
  }
215
341
 
216
- // 3. If we don't have data in the store, check if a request is in
217
- // flight for the fragment's parent query, or for another operation
218
- // that may affect the parent's query data, such as a mutation
219
- // or subscription. If a promise exists, cache the promise and use it
220
- // to suspend.
221
- const networkPromise = this._getAndSavePromiseForFragmentRequestInFlight(
222
- fragmentIdentifier,
223
- fragmentOwner,
224
- );
225
- if (networkPromise != null) {
226
- throw networkPromise;
342
+ // 3. If we don't have data in the store, there's two cases where we should
343
+ // suspend to await the data: First if any client edges were traversed where
344
+ // the destination record was missing data; in that case we initiate a query
345
+ // here to fetch the missing data. Second, there may already be a request
346
+ // in flight for the fragment's parent query, or for another operation that
347
+ // may affect the parent's query data, such as a mutation or subscription.
348
+ // For any of these cases we can get a promise, which we will cache and
349
+ // suspend on.
350
+
351
+ // First, initiate a query for any client edges that were missing data:
352
+ let clientEdgeRequests: ?Array<RequestDescriptor> = null;
353
+ if (
354
+ RelayFeatureFlags.ENABLE_CLIENT_EDGES &&
355
+ hasMissingClientEdges(snapshot)
356
+ ) {
357
+ clientEdgeRequests = [];
358
+ const queryResource = getQueryResourceForEnvironment(this._environment);
359
+ const queryResults = [];
360
+ singularOrPluralForEach(snapshot, snap => {
361
+ snap.missingClientEdges?.forEach(
362
+ ({request, clientEdgeDestinationID}) => {
363
+ const {queryResult, requestDescriptor} =
364
+ this._performClientEdgeQuery(
365
+ queryResource,
366
+ fragmentNode,
367
+ fragmentRef,
368
+ request,
369
+ clientEdgeDestinationID,
370
+ );
371
+ queryResults.push(queryResult);
372
+ clientEdgeRequests?.push(requestDescriptor);
373
+ },
374
+ );
375
+ });
376
+ // Store the query so that it can be retained when our own fragment is
377
+ // subscribed to. This merges with any existing query results:
378
+ invariant(
379
+ this._clientEdgeQueryResultsCache != null,
380
+ 'Client edge query result cache should exist when ENABLE_CLIENT_EDGES is on.',
381
+ );
382
+ this._clientEdgeQueryResultsCache.recordQueryResults(
383
+ fragmentIdentifier,
384
+ queryResults,
385
+ );
386
+ }
387
+ let clientEdgePromises = null;
388
+ if (RelayFeatureFlags.ENABLE_CLIENT_EDGES && clientEdgeRequests) {
389
+ clientEdgePromises = clientEdgeRequests
390
+ .map(request => getPromiseForActiveRequest(this._environment, request))
391
+ .filter(p => p != null);
392
+ }
393
+
394
+ // Finally look for operations in flight for our parent query:
395
+ const fragmentOwner =
396
+ fragmentSelector.kind === 'PluralReaderSelector'
397
+ ? fragmentSelector.selectors[0].owner
398
+ : fragmentSelector.owner;
399
+ const parentQueryPromiseResult =
400
+ this._getAndSavePromiseForFragmentRequestInFlight(
401
+ fragmentIdentifier,
402
+ fragmentNode,
403
+ fragmentOwner,
404
+ fragmentResult,
405
+ );
406
+ const parentQueryPromiseResultPromise = parentQueryPromiseResult?.promise; // for refinement
407
+
408
+ if (
409
+ clientEdgePromises?.length ||
410
+ isPromise(parentQueryPromiseResultPromise)
411
+ ) {
412
+ environment.__log({
413
+ name: 'suspense.fragment',
414
+ data: fragmentResult.data,
415
+ fragment: fragmentNode,
416
+ isRelayHooks: true,
417
+ isPromiseCached: false,
418
+ isMissingData: fragmentResult.isMissingData,
419
+ pendingOperations: [
420
+ ...(parentQueryPromiseResult?.pendingOperations ?? []),
421
+ ...(clientEdgeRequests ?? []),
422
+ ],
423
+ });
424
+ throw clientEdgePromises?.length
425
+ ? Promise.all([parentQueryPromiseResultPromise, ...clientEdgePromises])
426
+ : parentQueryPromiseResultPromise;
227
427
  }
228
428
 
229
429
  this._reportMissingRequiredFieldsInSnapshot(snapshot);
230
- return getFragmentResult(fragmentIdentifier, snapshot);
430
+ return getFragmentResult(fragmentIdentifier, snapshot, storeEpoch);
431
+ }
432
+
433
+ _performClientEdgeQuery(
434
+ queryResource: QueryResource,
435
+ fragmentNode: ReaderFragment,
436
+ fragmentRef: mixed,
437
+ request: ConcreteRequest,
438
+ clientEdgeDestinationID: DataID,
439
+ ) {
440
+ const originalVariables = getVariablesFromFragment(
441
+ fragmentNode,
442
+ fragmentRef,
443
+ );
444
+ const variables = {
445
+ ...originalVariables,
446
+ id: clientEdgeDestinationID, // TODO should be a reserved name
447
+ };
448
+ const operation = createOperationDescriptor(
449
+ request,
450
+ variables,
451
+ {}, // TODO cacheConfig should probably inherent from parent operation
452
+ );
453
+ const fetchObservable = fetchQuery(this._environment, operation);
454
+ const queryResult = queryResource.prepare(
455
+ operation,
456
+ fetchObservable,
457
+ // TODO should inherent render policy etc. from parent operation
458
+ );
459
+ return {
460
+ requestDescriptor: operation.request,
461
+ queryResult,
462
+ };
231
463
  }
232
464
 
233
465
  _reportMissingRequiredFieldsInSnapshot(snapshot: SingularOrPluralSnapshot) {
@@ -255,15 +487,16 @@ class FragmentResourceImpl {
255
487
  fragmentRefs: {[string]: mixed, ...},
256
488
  componentDisplayName: string,
257
489
  ): {[string]: FragmentResult, ...} {
258
- return mapObject(fragmentNodes, (fragmentNode, fragmentKey) => {
259
- const fragmentRef = fragmentRefs[fragmentKey];
260
- return this.read(
261
- fragmentNode,
262
- fragmentRef,
490
+ const result = {};
491
+ for (const key in fragmentNodes) {
492
+ result[key] = this.read(
493
+ fragmentNodes[key],
494
+ fragmentRefs[key],
263
495
  componentDisplayName,
264
- fragmentKey,
496
+ key,
265
497
  );
266
- });
498
+ }
499
+ return result;
267
500
  }
268
501
 
269
502
  subscribe(fragmentResult: FragmentResult, callback: () => void): Disposable {
@@ -276,9 +509,8 @@ class FragmentResourceImpl {
276
509
 
277
510
  // 1. Check for any updates missed during render phase
278
511
  // TODO(T44066760): More efficiently detect if we missed an update
279
- const [didMissUpdates, currentSnapshot] = this.checkMissedUpdates(
280
- fragmentResult,
281
- );
512
+ const [didMissUpdates, currentSnapshot] =
513
+ this.checkMissedUpdates(fragmentResult);
282
514
 
283
515
  // 2. If an update was missed, notify the component so it updates with
284
516
  // the latest data.
@@ -287,7 +519,7 @@ class FragmentResourceImpl {
287
519
  }
288
520
 
289
521
  // 3. Establish subscriptions on the snapshot(s)
290
- const dataSubscriptions = [];
522
+ const disposables = [];
291
523
  if (Array.isArray(renderedSnapshot)) {
292
524
  invariant(
293
525
  Array.isArray(currentSnapshot),
@@ -295,13 +527,15 @@ class FragmentResourceImpl {
295
527
  "If you're seeing this, this is likely a bug in Relay.",
296
528
  );
297
529
  currentSnapshot.forEach((snapshot, idx) => {
298
- dataSubscriptions.push(
530
+ disposables.push(
299
531
  environment.subscribe(snapshot, latestSnapshot => {
532
+ const storeEpoch = environment.getStore().getEpoch();
300
533
  this._updatePluralSnapshot(
301
534
  cacheKey,
302
535
  currentSnapshot,
303
536
  latestSnapshot,
304
537
  idx,
538
+ storeEpoch,
305
539
  );
306
540
  callback();
307
541
  }),
@@ -313,20 +547,32 @@ class FragmentResourceImpl {
313
547
  'Relay: Expected snapshot to be singular. ' +
314
548
  "If you're seeing this, this is likely a bug in Relay.",
315
549
  );
316
- dataSubscriptions.push(
550
+ disposables.push(
317
551
  environment.subscribe(currentSnapshot, latestSnapshot => {
318
- this._cache.set(
319
- cacheKey,
320
- getFragmentResult(cacheKey, latestSnapshot),
321
- );
552
+ const storeEpoch = environment.getStore().getEpoch();
553
+ this._cache.set(cacheKey, {
554
+ kind: 'done',
555
+ result: getFragmentResult(cacheKey, latestSnapshot, storeEpoch),
556
+ });
322
557
  callback();
323
558
  }),
324
559
  );
325
560
  }
326
561
 
562
+ if (RelayFeatureFlags.ENABLE_CLIENT_EDGES) {
563
+ const clientEdgeQueryResults =
564
+ this._clientEdgeQueryResultsCache?.get(cacheKey) ?? undefined;
565
+ if (clientEdgeQueryResults?.length) {
566
+ const queryResource = getQueryResourceForEnvironment(this._environment);
567
+ clientEdgeQueryResults.forEach(queryResult => {
568
+ disposables.push(queryResource.retain(queryResult));
569
+ });
570
+ }
571
+ }
572
+
327
573
  return {
328
574
  dispose: () => {
329
- dataSubscriptions.map(s => s.dispose());
575
+ disposables.forEach(s => s.dispose());
330
576
  this._cache.delete(cacheKey);
331
577
  },
332
578
  };
@@ -352,18 +598,25 @@ class FragmentResourceImpl {
352
598
  fragmentResult: FragmentResult,
353
599
  ): [boolean, SingularOrPluralSnapshot | null] {
354
600
  const environment = this._environment;
355
- const {cacheKey} = fragmentResult;
356
601
  const renderedSnapshot = fragmentResult.snapshot;
357
602
  if (!renderedSnapshot) {
358
603
  return [false, null];
359
604
  }
360
605
 
361
- let didMissUpdates = false;
606
+ let storeEpoch = null;
607
+ // Bail out if the store hasn't been written since last read
608
+ storeEpoch = environment.getStore().getEpoch();
609
+ if (fragmentResult.storeEpoch === storeEpoch) {
610
+ return [false, fragmentResult.snapshot];
611
+ }
612
+
613
+ const {cacheKey} = fragmentResult;
362
614
 
363
615
  if (Array.isArray(renderedSnapshot)) {
616
+ let didMissUpdates = false;
364
617
  const currentSnapshots = [];
365
618
  renderedSnapshot.forEach((snapshot, idx) => {
366
- let currentSnapshot = environment.lookup(snapshot.selector);
619
+ let currentSnapshot: Snapshot = environment.lookup(snapshot.selector);
367
620
  const renderData = snapshot.data;
368
621
  const currentData = currentSnapshot.data;
369
622
  const updatedData = recycleNodesInto(renderData, currentData);
@@ -373,35 +626,40 @@ class FragmentResourceImpl {
373
626
  }
374
627
  currentSnapshots[idx] = currentSnapshot;
375
628
  });
629
+ // Only update the cache when the data is changed to avoid
630
+ // returning different `data` instances
376
631
  if (didMissUpdates) {
377
- this._cache.set(
378
- cacheKey,
379
- getFragmentResult(cacheKey, currentSnapshots),
380
- );
632
+ this._cache.set(cacheKey, {
633
+ kind: 'done',
634
+ result: getFragmentResult(cacheKey, currentSnapshots, storeEpoch),
635
+ });
381
636
  }
382
637
  return [didMissUpdates, currentSnapshots];
383
638
  }
384
- let currentSnapshot = environment.lookup(renderedSnapshot.selector);
639
+ const currentSnapshot = environment.lookup(renderedSnapshot.selector);
385
640
  const renderData = renderedSnapshot.data;
386
641
  const currentData = currentSnapshot.data;
387
642
  const updatedData = recycleNodesInto(renderData, currentData);
388
- currentSnapshot = {
643
+ const updatedCurrentSnapshot: Snapshot = {
389
644
  data: updatedData,
390
645
  isMissingData: currentSnapshot.isMissingData,
646
+ missingClientEdges: currentSnapshot.missingClientEdges,
391
647
  seenRecords: currentSnapshot.seenRecords,
392
648
  selector: currentSnapshot.selector,
393
649
  missingRequiredFields: currentSnapshot.missingRequiredFields,
394
650
  };
395
651
  if (updatedData !== renderData) {
396
- this._cache.set(cacheKey, getFragmentResult(cacheKey, currentSnapshot));
397
- didMissUpdates = true;
652
+ this._cache.set(cacheKey, {
653
+ kind: 'done',
654
+ result: getFragmentResult(cacheKey, updatedCurrentSnapshot, storeEpoch),
655
+ });
398
656
  }
399
- return [didMissUpdates, currentSnapshot];
657
+ return [updatedData !== renderData, updatedCurrentSnapshot];
400
658
  }
401
659
 
402
660
  checkMissedUpdatesSpec(fragmentResults: {
403
661
  [string]: FragmentResult,
404
- ...,
662
+ ...
405
663
  }): boolean {
406
664
  return Object.keys(fragmentResults).some(
407
665
  key => this.checkMissedUpdates(fragmentResults[key])[0],
@@ -410,19 +668,27 @@ class FragmentResourceImpl {
410
668
 
411
669
  _getAndSavePromiseForFragmentRequestInFlight(
412
670
  cacheKey: string,
671
+ fragmentNode: ReaderFragment,
413
672
  fragmentOwner: RequestDescriptor,
414
- ): Promise<void> | null {
415
- const environment = this._environment;
416
- const networkPromise =
417
- getPromiseForActiveRequest(environment, fragmentOwner) ??
418
- getPromiseForPendingOperationAffectingOwner(environment, fragmentOwner);
419
-
420
- if (!networkPromise) {
673
+ fragmentResult: FragmentResult,
674
+ ): {|
675
+ promise: Promise<void>,
676
+ pendingOperations: $ReadOnlyArray<RequestDescriptor>,
677
+ |} | null {
678
+ const pendingOperationsResult = getPendingOperationsForFragment(
679
+ this._environment,
680
+ fragmentNode,
681
+ fragmentOwner,
682
+ );
683
+ if (pendingOperationsResult == null) {
421
684
  return null;
422
685
  }
686
+
423
687
  // When the Promise for the request resolves, we need to make sure to
424
688
  // update the cache with the latest data available in the store before
425
689
  // resolving the Promise
690
+ const networkPromise = pendingOperationsResult.promise;
691
+ const pendingOperations = pendingOperationsResult.pendingOperations;
426
692
  const promise = networkPromise
427
693
  .then(() => {
428
694
  this._cache.delete(cacheKey);
@@ -430,11 +696,15 @@ class FragmentResourceImpl {
430
696
  .catch((error: Error) => {
431
697
  this._cache.delete(cacheKey);
432
698
  });
433
- this._cache.set(cacheKey, promise);
434
-
435
699
  // $FlowExpectedError[prop-missing] Expando to annotate Promises.
436
- promise.displayName = 'Relay(' + fragmentOwner.node.params.name + ')';
437
- return promise;
700
+ promise.displayName = networkPromise.displayName;
701
+ this._cache.set(cacheKey, {
702
+ kind: 'pending',
703
+ pendingOperations,
704
+ promise,
705
+ result: fragmentResult,
706
+ });
707
+ return {promise, pendingOperations};
438
708
  }
439
709
 
440
710
  _updatePluralSnapshot(
@@ -442,6 +712,7 @@ class FragmentResourceImpl {
442
712
  baseSnapshots: $ReadOnlyArray<Snapshot>,
443
713
  latestSnapshot: Snapshot,
444
714
  idx: number,
715
+ storeEpoch: number,
445
716
  ): void {
446
717
  const currentFragmentResult = this._cache.get(cacheKey);
447
718
  if (isPromise(currentFragmentResult)) {
@@ -449,7 +720,7 @@ class FragmentResourceImpl {
449
720
  return;
450
721
  }
451
722
 
452
- const currentSnapshot = currentFragmentResult?.snapshot;
723
+ const currentSnapshot = currentFragmentResult?.result?.snapshot;
453
724
  if (currentSnapshot && !Array.isArray(currentSnapshot)) {
454
725
  reportInvalidCachedData(latestSnapshot.selector.node.name);
455
726
  return;
@@ -459,7 +730,10 @@ class FragmentResourceImpl {
459
730
  ? [...currentSnapshot]
460
731
  : [...baseSnapshots];
461
732
  nextSnapshots[idx] = latestSnapshot;
462
- this._cache.set(cacheKey, getFragmentResult(cacheKey, nextSnapshots));
733
+ this._cache.set(cacheKey, {
734
+ kind: 'done',
735
+ result: getFragmentResult(cacheKey, nextSnapshots, storeEpoch),
736
+ });
463
737
  }
464
738
  }
465
739