react-relay 11.0.2 → 13.0.0-rc.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (177) hide show
  1. package/README.md +47 -0
  2. package/ReactRelayContainerUtils.js.flow +1 -1
  3. package/ReactRelayContext.js +1 -1
  4. package/ReactRelayContext.js.flow +3 -4
  5. package/ReactRelayFragmentContainer.js.flow +25 -25
  6. package/ReactRelayFragmentMockRenderer.js.flow +2 -2
  7. package/ReactRelayLocalQueryRenderer.js.flow +7 -8
  8. package/ReactRelayPaginationContainer.js.flow +112 -59
  9. package/ReactRelayQueryFetcher.js.flow +10 -11
  10. package/ReactRelayQueryRenderer.js.flow +116 -82
  11. package/ReactRelayQueryRendererContext.js.flow +1 -1
  12. package/ReactRelayRefetchContainer.js.flow +42 -39
  13. package/ReactRelayTestMocker.js.flow +17 -15
  14. package/ReactRelayTypes.js.flow +11 -11
  15. package/RelayContext.js.flow +4 -4
  16. package/__flowtests__/ReactRelayFragmentContainer-flowtest.js.flow +2 -3
  17. package/__flowtests__/ReactRelayPaginationContainer-flowtest.js.flow +12 -8
  18. package/__flowtests__/ReactRelayRefetchContainer-flowtest.js.flow +11 -7
  19. package/__flowtests__/RelayModern-flowtest.js.flow +79 -47
  20. package/__flowtests__/RelayModernFlowtest_badref.graphql.js.flow +6 -5
  21. package/__flowtests__/RelayModernFlowtest_notref.graphql.js.flow +6 -5
  22. package/__flowtests__/RelayModernFlowtest_user.graphql.js.flow +5 -4
  23. package/__flowtests__/RelayModernFlowtest_users.graphql.js.flow +5 -4
  24. package/__flowtests__/__generated__/ReactRelayFragmentContainerFlowtest_viewer.graphql.js.flow +72 -0
  25. package/__flowtests__/__generated__/ReactRelayFragmentContainerFlowtest_viewer2.graphql.js.flow +72 -0
  26. package/__flowtests__/__generated__/ReactRelayPaginationContainerFlowtestQuery.graphql.js.flow +227 -0
  27. package/__flowtests__/__generated__/ReactRelayPaginationContainerFlowtest_viewer.graphql.js.flow +164 -0
  28. package/__flowtests__/__generated__/ReactRelayRefetchContainerFlowtestQuery.graphql.js.flow +227 -0
  29. package/__flowtests__/__generated__/ReactRelayRefetchContainerFlowtest_viewer.graphql.js.flow +164 -0
  30. package/__flowtests__/__generated__/RelayModernFlowtest_badref.graphql.js.flow +66 -0
  31. package/__flowtests__/__generated__/RelayModernFlowtest_notref.graphql.js.flow +66 -0
  32. package/__flowtests__/__generated__/RelayModernFlowtest_user.graphql.js.flow +59 -0
  33. package/__flowtests__/__generated__/RelayModernFlowtest_users.graphql.js.flow +61 -0
  34. package/assertFragmentMap.js.flow +3 -3
  35. package/buildReactRelayContainer.js.flow +12 -11
  36. package/getRootVariablesForFragments.js.flow +3 -5
  37. package/hooks.js +1 -1
  38. package/hooks.js.flow +6 -7
  39. package/index.js +1 -1
  40. package/index.js.flow +7 -8
  41. package/isRelayEnvironment.js.flow +1 -1
  42. package/jest-react/enqueueTask.js.flow +56 -0
  43. package/jest-react/index.js.flow +12 -0
  44. package/jest-react/internalAct.js.flow +138 -0
  45. package/legacy.js +1 -1
  46. package/legacy.js.flow +1 -1
  47. package/lib/ReactRelayContainerUtils.js +1 -1
  48. package/lib/ReactRelayContext.js +1 -1
  49. package/lib/ReactRelayFragmentContainer.js +22 -16
  50. package/lib/ReactRelayFragmentMockRenderer.js +3 -3
  51. package/lib/ReactRelayLocalQueryRenderer.js +8 -9
  52. package/lib/ReactRelayPaginationContainer.js +97 -39
  53. package/lib/ReactRelayQueryFetcher.js +3 -3
  54. package/lib/ReactRelayQueryRenderer.js +87 -54
  55. package/lib/ReactRelayQueryRendererContext.js +1 -1
  56. package/lib/ReactRelayRefetchContainer.js +39 -26
  57. package/lib/ReactRelayTestMocker.js +8 -9
  58. package/lib/ReactRelayTypes.js +1 -1
  59. package/lib/RelayContext.js +4 -3
  60. package/lib/assertFragmentMap.js +3 -2
  61. package/lib/buildReactRelayContainer.js +8 -8
  62. package/lib/getRootVariablesForFragments.js +2 -3
  63. package/lib/hooks.js +6 -6
  64. package/lib/index.js +8 -8
  65. package/lib/isRelayEnvironment.js +1 -1
  66. package/lib/jest-react/enqueueTask.js +53 -0
  67. package/lib/jest-react/index.js +13 -0
  68. package/lib/jest-react/internalAct.js +115 -0
  69. package/lib/legacy.js +1 -1
  70. package/lib/multi-actor/ActorChange.js +30 -0
  71. package/lib/multi-actor/index.js +11 -0
  72. package/lib/multi-actor/useRelayActorEnvironment.js +29 -0
  73. package/lib/readContext.js +1 -1
  74. package/lib/relay-hooks/EntryPointContainer.react.js +4 -4
  75. package/lib/relay-hooks/EntryPointTypes.flow.js +1 -1
  76. package/lib/relay-hooks/FragmentResource.js +342 -89
  77. package/lib/relay-hooks/InternalLogger.js +1 -1
  78. package/lib/relay-hooks/LRUCache.js +1 -1
  79. package/lib/relay-hooks/LazyLoadEntryPointContainer_DEPRECATED.react.js +5 -5
  80. package/lib/relay-hooks/MatchContainer.js +2 -2
  81. package/lib/relay-hooks/ProfilerContext.js +1 -1
  82. package/lib/relay-hooks/QueryResource.js +172 -29
  83. package/lib/relay-hooks/RelayEnvironmentProvider.js +6 -4
  84. package/lib/relay-hooks/SuspenseResource.js +130 -0
  85. package/lib/relay-hooks/loadEntryPoint.js +1 -1
  86. package/lib/relay-hooks/loadQuery.js +42 -20
  87. package/lib/relay-hooks/preloadQuery_DEPRECATED.js +25 -16
  88. package/lib/relay-hooks/prepareEntryPoint_DEPRECATED.js +1 -1
  89. package/lib/relay-hooks/useBlockingPaginationFragment.js +5 -6
  90. package/lib/relay-hooks/useEntryPointLoader.js +3 -3
  91. package/lib/relay-hooks/useFetchTrackingRef.js +3 -2
  92. package/lib/relay-hooks/useFragment.js +7 -7
  93. package/lib/relay-hooks/useFragmentNode.js +5 -5
  94. package/lib/relay-hooks/useIsMountedRef.js +1 -1
  95. package/lib/relay-hooks/useIsOperationNodeActive.js +3 -3
  96. package/lib/relay-hooks/useIsParentQueryActive.js +1 -1
  97. package/lib/relay-hooks/useLazyLoadQuery.js +4 -4
  98. package/lib/relay-hooks/useLazyLoadQueryNode.js +11 -5
  99. package/lib/relay-hooks/useLoadMoreFunction.js +9 -13
  100. package/lib/relay-hooks/useMemoOperationDescriptor.js +3 -3
  101. package/lib/relay-hooks/useMemoVariables.js +3 -3
  102. package/lib/relay-hooks/useMutation.js +18 -7
  103. package/lib/relay-hooks/usePaginationFragment.js +3 -4
  104. package/lib/relay-hooks/usePreloadedQuery.js +6 -6
  105. package/lib/relay-hooks/useQueryLoader.js +31 -11
  106. package/lib/relay-hooks/useRefetchableFragment.js +1 -1
  107. package/lib/relay-hooks/useRefetchableFragmentNode.js +14 -18
  108. package/lib/relay-hooks/useRelayEnvironment.js +3 -3
  109. package/lib/relay-hooks/useStaticFragmentNodeWarning.js +3 -3
  110. package/lib/relay-hooks/useSubscribeToInvalidationState.js +3 -2
  111. package/lib/relay-hooks/useSubscription.js +11 -8
  112. package/multi-actor/ActorChange.js.flow +58 -0
  113. package/multi-actor/index.js.flow +14 -0
  114. package/multi-actor/useRelayActorEnvironment.js.flow +49 -0
  115. package/package.json +3 -3
  116. package/react-relay-hooks.js +2 -2
  117. package/react-relay-hooks.min.js +2 -2
  118. package/react-relay-legacy.js +2 -2
  119. package/react-relay-legacy.min.js +2 -2
  120. package/react-relay.js +2 -2
  121. package/react-relay.min.js +2 -2
  122. package/readContext.js.flow +1 -1
  123. package/relay-hooks/EntryPointContainer.react.js.flow +9 -16
  124. package/relay-hooks/EntryPointTypes.flow.js.flow +25 -26
  125. package/relay-hooks/FragmentResource.js.flow +359 -93
  126. package/relay-hooks/InternalLogger.js.flow +1 -1
  127. package/relay-hooks/LRUCache.js.flow +1 -1
  128. package/relay-hooks/LazyLoadEntryPointContainer_DEPRECATED.react.js.flow +33 -47
  129. package/relay-hooks/MatchContainer.js.flow +4 -3
  130. package/relay-hooks/ProfilerContext.js.flow +1 -1
  131. package/relay-hooks/QueryResource.js.flow +217 -26
  132. package/relay-hooks/RelayEnvironmentProvider.js.flow +15 -5
  133. package/relay-hooks/SuspenseResource.js.flow +115 -0
  134. package/relay-hooks/__flowtests__/EntryPointTypes/EntryPointElementConfig-flowtest.js.flow +5 -4
  135. package/relay-hooks/__flowtests__/EntryPointTypes/NestedEntrypoints-flowtest.js.flow +2 -2
  136. package/relay-hooks/__flowtests__/__generated__/useFragmentFlowtest_user.graphql.js.flow +59 -0
  137. package/relay-hooks/__flowtests__/__generated__/useFragmentFlowtest_users.graphql.js.flow +61 -0
  138. package/relay-hooks/__flowtests__/useBlockingPaginationFragment-flowtest.js.flow +11 -10
  139. package/relay-hooks/__flowtests__/useFragment-flowtest.js.flow +55 -32
  140. package/relay-hooks/__flowtests__/usePaginationFragment-flowtest.js.flow +11 -10
  141. package/relay-hooks/__flowtests__/useRefetchableFragment-flowtest.js.flow +11 -10
  142. package/relay-hooks/__flowtests__/utils.js.flow +21 -32
  143. package/relay-hooks/loadEntryPoint.js.flow +7 -13
  144. package/relay-hooks/loadQuery.js.flow +50 -32
  145. package/relay-hooks/preloadQuery_DEPRECATED.js.flow +31 -22
  146. package/relay-hooks/prepareEntryPoint_DEPRECATED.js.flow +7 -13
  147. package/relay-hooks/useBlockingPaginationFragment.js.flow +14 -12
  148. package/relay-hooks/useEntryPointLoader.js.flow +8 -11
  149. package/relay-hooks/useFetchTrackingRef.js.flow +3 -3
  150. package/relay-hooks/useFragment.js.flow +31 -62
  151. package/relay-hooks/useFragmentNode.js.flow +6 -8
  152. package/relay-hooks/useIsMountedRef.js.flow +1 -1
  153. package/relay-hooks/useIsOperationNodeActive.js.flow +4 -6
  154. package/relay-hooks/useIsParentQueryActive.js.flow +4 -5
  155. package/relay-hooks/useLazyLoadQuery.js.flow +14 -16
  156. package/relay-hooks/useLazyLoadQueryNode.js.flow +20 -14
  157. package/relay-hooks/useLoadMoreFunction.js.flow +21 -30
  158. package/relay-hooks/useMemoOperationDescriptor.js.flow +6 -8
  159. package/relay-hooks/useMemoVariables.js.flow +7 -7
  160. package/relay-hooks/useMutation.js.flow +27 -27
  161. package/relay-hooks/usePaginationFragment.js.flow +39 -45
  162. package/relay-hooks/usePreloadedQuery.js.flow +14 -20
  163. package/relay-hooks/useQueryLoader.js.flow +42 -23
  164. package/relay-hooks/useRefetchableFragment.js.flow +8 -9
  165. package/relay-hooks/useRefetchableFragmentNode.js.flow +25 -33
  166. package/relay-hooks/useRelayEnvironment.js.flow +3 -5
  167. package/relay-hooks/useStaticFragmentNodeWarning.js.flow +3 -4
  168. package/relay-hooks/useSubscribeToInvalidationState.js.flow +4 -7
  169. package/relay-hooks/useSubscription.js.flow +21 -11
  170. package/lib/relay-hooks/getPaginationMetadata.js +0 -41
  171. package/lib/relay-hooks/getPaginationVariables.js +0 -67
  172. package/lib/relay-hooks/getRefetchMetadata.js +0 -36
  173. package/lib/relay-hooks/getValueAtPath.js +0 -51
  174. package/relay-hooks/getPaginationMetadata.js.flow +0 -74
  175. package/relay-hooks/getPaginationVariables.js.flow +0 -110
  176. package/relay-hooks/getRefetchMetadata.js.flow +0 -80
  177. package/relay-hooks/getValueAtPath.js.flow +0 -46
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Copyright (c) Facebook, Inc. and its affiliates.
2
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
3
3
  *
4
4
  * This source code is licensed under the MIT license found in the
5
5
  * LICENSE file in the root directory of this source tree.
@@ -13,31 +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
-
20
32
  const {
21
- __internal: {getPromiseForActiveRequest},
33
+ RelayFeatureFlags,
34
+ __internal: {fetchQuery, getPromiseForActiveRequest},
35
+ createOperationDescriptor,
22
36
  getFragmentIdentifier,
37
+ getPendingOperationsForFragment,
23
38
  getSelector,
39
+ getVariablesFromFragment,
24
40
  isPromise,
25
41
  recycleNodesInto,
26
42
  reportMissingRequiredFields,
27
43
  } = require('relay-runtime');
28
44
 
29
- import type {Cache} from './LRUCache';
30
- import type {
31
- Disposable,
32
- IEnvironment,
33
- ReaderFragment,
34
- RequestDescriptor,
35
- Snapshot,
36
- } from 'relay-runtime';
37
-
38
45
  export type FragmentResource = FragmentResourceImpl;
39
46
 
40
- 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
+ >;
41
56
 
42
57
  const WEAKMAP_SUPPORTED = typeof WeakMap === 'function';
43
58
  interface IMap<K, V> {
@@ -46,10 +61,13 @@ interface IMap<K, V> {
46
61
  }
47
62
 
48
63
  type SingularOrPluralSnapshot = Snapshot | $ReadOnlyArray<Snapshot>;
64
+
49
65
  opaque type FragmentResult: {data: mixed, ...} = {|
50
66
  cacheKey: string,
51
67
  data: mixed,
68
+ isMissingData: boolean,
52
69
  snapshot: SingularOrPluralSnapshot | null,
70
+ storeEpoch: number,
53
71
  |};
54
72
 
55
73
  // TODO: Fix to not rely on LRU. If the number of active fragments exceeds this
@@ -60,39 +78,123 @@ const CACHE_CAPACITY = 1000000;
60
78
  // this is frozen so that users don't accidentally push data into the array
61
79
  const CONSTANT_READONLY_EMPTY_ARRAY = Object.freeze([]);
62
80
 
63
- function isMissingData(snapshot: SingularOrPluralSnapshot) {
81
+ function isMissingData(snapshot: SingularOrPluralSnapshot): boolean {
64
82
  if (Array.isArray(snapshot)) {
65
83
  return snapshot.some(s => s.isMissingData);
66
84
  }
67
85
  return snapshot.isMissingData;
68
86
  }
69
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
+
70
106
  function getFragmentResult(
71
107
  cacheKey: string,
72
108
  snapshot: SingularOrPluralSnapshot,
109
+ storeEpoch: number,
73
110
  ): FragmentResult {
74
111
  if (Array.isArray(snapshot)) {
75
- 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
+ };
76
119
  }
77
- return {cacheKey, snapshot, data: snapshot.data};
120
+ return {
121
+ cacheKey,
122
+ snapshot,
123
+ data: snapshot.data,
124
+ isMissingData: isMissingData(snapshot),
125
+ storeEpoch,
126
+ };
78
127
  }
79
128
 
80
- function getPromiseForPendingOperationAffectingOwner(
81
- environment: IEnvironment,
82
- request: RequestDescriptor,
83
- ): Promise<void> | null {
84
- return environment
85
- .getOperationTracker()
86
- .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
+ }
87
183
  }
88
184
 
89
185
  class FragmentResourceImpl {
90
186
  _environment: IEnvironment;
91
187
  _cache: FragmentResourceCache;
188
+ _clientEdgeQueryResultsCache: void | ClientEdgeQueryResultsCache;
92
189
 
93
190
  constructor(environment: IEnvironment) {
94
191
  this._environment = environment;
95
192
  this._cache = LRUCache.create(CACHE_CAPACITY);
193
+ if (RelayFeatureFlags.ENABLE_CLIENT_EDGES) {
194
+ this._clientEdgeQueryResultsCache = new ClientEdgeQueryResultsCache(
195
+ environment,
196
+ );
197
+ }
96
198
  }
97
199
 
98
200
  /**
@@ -133,9 +235,17 @@ class FragmentResourceImpl {
133
235
  // This is a convenience when consuming fragments via a HOC API, when the
134
236
  // prop corresponding to the fragment ref might be passed as null.
135
237
  if (fragmentRef == null) {
136
- 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
+ };
137
245
  }
138
246
 
247
+ const storeEpoch = environment.getStore().getEpoch();
248
+
139
249
  // If fragmentRef is plural, ensure that it is an array.
140
250
  // If it's empty, return the empty array directly before doing any more work.
141
251
  if (fragmentNode?.metadata?.plural === true) {
@@ -153,7 +263,9 @@ class FragmentResourceImpl {
153
263
  return {
154
264
  cacheKey: fragmentIdentifier,
155
265
  data: CONSTANT_READONLY_EMPTY_ARRAY,
266
+ isMissingData: false,
156
267
  snapshot: CONSTANT_READONLY_EMPTY_ARRAY,
268
+ storeEpoch,
157
269
  };
158
270
  }
159
271
  }
@@ -163,12 +275,24 @@ class FragmentResourceImpl {
163
275
  // 1. Check if there's a cached value for this fragment
164
276
  const cachedValue = this._cache.get(fragmentIdentifier);
165
277
  if (cachedValue != null) {
166
- if (isPromise(cachedValue)) {
167
- 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;
168
289
  }
169
- if (cachedValue.snapshot) {
170
- this._reportMissingRequiredFieldsInSnapshot(cachedValue.snapshot);
171
- return cachedValue;
290
+
291
+ if (cachedValue.kind === 'done' && cachedValue.result.snapshot) {
292
+ this._reportMissingRequiredFieldsInSnapshot(
293
+ cachedValue.result.snapshot,
294
+ );
295
+ return cachedValue.result;
172
296
  }
173
297
  }
174
298
 
@@ -200,34 +324,142 @@ class FragmentResourceImpl {
200
324
  ? fragmentSelector.selectors.map(s => environment.lookup(s))
201
325
  : environment.lookup(fragmentSelector);
202
326
 
327
+ const fragmentResult = getFragmentResult(
328
+ fragmentIdentifier,
329
+ snapshot,
330
+ storeEpoch,
331
+ );
332
+ if (!fragmentResult.isMissingData) {
333
+ this._reportMissingRequiredFieldsInSnapshot(snapshot);
334
+
335
+ this._cache.set(fragmentIdentifier, {
336
+ kind: 'done',
337
+ result: fragmentResult,
338
+ });
339
+ return fragmentResult;
340
+ }
341
+
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:
203
395
  const fragmentOwner =
204
396
  fragmentSelector.kind === 'PluralReaderSelector'
205
397
  ? fragmentSelector.selectors[0].owner
206
398
  : fragmentSelector.owner;
207
-
208
- if (!isMissingData(snapshot)) {
209
- this._reportMissingRequiredFieldsInSnapshot(snapshot);
210
- const fragmentResult = getFragmentResult(fragmentIdentifier, snapshot);
211
- this._cache.set(fragmentIdentifier, fragmentResult);
212
- return fragmentResult;
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;
213
427
  }
214
428
 
215
- // 3. If we don't have data in the store, check if a request is in
216
- // flight for the fragment's parent query, or for another operation
217
- // that may affect the parent's query data, such as a mutation
218
- // or subscription. If a promise exists, cache the promise and use it
219
- // to suspend.
220
- const networkPromise = this._getAndSavePromiseForFragmentRequestInFlight(
221
- fragmentIdentifier,
429
+ this._reportMissingRequiredFieldsInSnapshot(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(
222
441
  fragmentNode,
223
- fragmentOwner,
442
+ fragmentRef,
224
443
  );
225
- if (networkPromise != null) {
226
- throw networkPromise;
227
- }
228
-
229
- this._reportMissingRequiredFieldsInSnapshot(snapshot);
230
- return getFragmentResult(fragmentIdentifier, snapshot);
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) {
@@ -277,9 +509,8 @@ class FragmentResourceImpl {
277
509
 
278
510
  // 1. Check for any updates missed during render phase
279
511
  // TODO(T44066760): More efficiently detect if we missed an update
280
- const [didMissUpdates, currentSnapshot] = this.checkMissedUpdates(
281
- fragmentResult,
282
- );
512
+ const [didMissUpdates, currentSnapshot] =
513
+ this.checkMissedUpdates(fragmentResult);
283
514
 
284
515
  // 2. If an update was missed, notify the component so it updates with
285
516
  // the latest data.
@@ -288,7 +519,7 @@ class FragmentResourceImpl {
288
519
  }
289
520
 
290
521
  // 3. Establish subscriptions on the snapshot(s)
291
- const dataSubscriptions = [];
522
+ const disposables = [];
292
523
  if (Array.isArray(renderedSnapshot)) {
293
524
  invariant(
294
525
  Array.isArray(currentSnapshot),
@@ -296,13 +527,15 @@ class FragmentResourceImpl {
296
527
  "If you're seeing this, this is likely a bug in Relay.",
297
528
  );
298
529
  currentSnapshot.forEach((snapshot, idx) => {
299
- dataSubscriptions.push(
530
+ disposables.push(
300
531
  environment.subscribe(snapshot, latestSnapshot => {
532
+ const storeEpoch = environment.getStore().getEpoch();
301
533
  this._updatePluralSnapshot(
302
534
  cacheKey,
303
535
  currentSnapshot,
304
536
  latestSnapshot,
305
537
  idx,
538
+ storeEpoch,
306
539
  );
307
540
  callback();
308
541
  }),
@@ -314,20 +547,32 @@ class FragmentResourceImpl {
314
547
  'Relay: Expected snapshot to be singular. ' +
315
548
  "If you're seeing this, this is likely a bug in Relay.",
316
549
  );
317
- dataSubscriptions.push(
550
+ disposables.push(
318
551
  environment.subscribe(currentSnapshot, latestSnapshot => {
319
- this._cache.set(
320
- cacheKey,
321
- getFragmentResult(cacheKey, latestSnapshot),
322
- );
552
+ const storeEpoch = environment.getStore().getEpoch();
553
+ this._cache.set(cacheKey, {
554
+ kind: 'done',
555
+ result: getFragmentResult(cacheKey, latestSnapshot, storeEpoch),
556
+ });
323
557
  callback();
324
558
  }),
325
559
  );
326
560
  }
327
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
+
328
573
  return {
329
574
  dispose: () => {
330
- dataSubscriptions.map(s => s.dispose());
575
+ disposables.forEach(s => s.dispose());
331
576
  this._cache.delete(cacheKey);
332
577
  },
333
578
  };
@@ -353,18 +598,25 @@ class FragmentResourceImpl {
353
598
  fragmentResult: FragmentResult,
354
599
  ): [boolean, SingularOrPluralSnapshot | null] {
355
600
  const environment = this._environment;
356
- const {cacheKey} = fragmentResult;
357
601
  const renderedSnapshot = fragmentResult.snapshot;
358
602
  if (!renderedSnapshot) {
359
603
  return [false, null];
360
604
  }
361
605
 
362
- 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;
363
614
 
364
615
  if (Array.isArray(renderedSnapshot)) {
616
+ let didMissUpdates = false;
365
617
  const currentSnapshots = [];
366
618
  renderedSnapshot.forEach((snapshot, idx) => {
367
- let currentSnapshot = environment.lookup(snapshot.selector);
619
+ let currentSnapshot: Snapshot = environment.lookup(snapshot.selector);
368
620
  const renderData = snapshot.data;
369
621
  const currentData = currentSnapshot.data;
370
622
  const updatedData = recycleNodesInto(renderData, currentData);
@@ -374,35 +626,40 @@ class FragmentResourceImpl {
374
626
  }
375
627
  currentSnapshots[idx] = currentSnapshot;
376
628
  });
629
+ // Only update the cache when the data is changed to avoid
630
+ // returning different `data` instances
377
631
  if (didMissUpdates) {
378
- this._cache.set(
379
- cacheKey,
380
- getFragmentResult(cacheKey, currentSnapshots),
381
- );
632
+ this._cache.set(cacheKey, {
633
+ kind: 'done',
634
+ result: getFragmentResult(cacheKey, currentSnapshots, storeEpoch),
635
+ });
382
636
  }
383
637
  return [didMissUpdates, currentSnapshots];
384
638
  }
385
- let currentSnapshot = environment.lookup(renderedSnapshot.selector);
639
+ const currentSnapshot = environment.lookup(renderedSnapshot.selector);
386
640
  const renderData = renderedSnapshot.data;
387
641
  const currentData = currentSnapshot.data;
388
642
  const updatedData = recycleNodesInto(renderData, currentData);
389
- currentSnapshot = {
643
+ const updatedCurrentSnapshot: Snapshot = {
390
644
  data: updatedData,
391
645
  isMissingData: currentSnapshot.isMissingData,
646
+ missingClientEdges: currentSnapshot.missingClientEdges,
392
647
  seenRecords: currentSnapshot.seenRecords,
393
648
  selector: currentSnapshot.selector,
394
649
  missingRequiredFields: currentSnapshot.missingRequiredFields,
395
650
  };
396
651
  if (updatedData !== renderData) {
397
- this._cache.set(cacheKey, getFragmentResult(cacheKey, currentSnapshot));
398
- didMissUpdates = true;
652
+ this._cache.set(cacheKey, {
653
+ kind: 'done',
654
+ result: getFragmentResult(cacheKey, updatedCurrentSnapshot, storeEpoch),
655
+ });
399
656
  }
400
- return [didMissUpdates, currentSnapshot];
657
+ return [updatedData !== renderData, updatedCurrentSnapshot];
401
658
  }
402
659
 
403
660
  checkMissedUpdatesSpec(fragmentResults: {
404
661
  [string]: FragmentResult,
405
- ...,
662
+ ...
406
663
  }): boolean {
407
664
  return Object.keys(fragmentResults).some(
408
665
  key => this.checkMissedUpdates(fragmentResults[key])[0],
@@ -413,18 +670,25 @@ class FragmentResourceImpl {
413
670
  cacheKey: string,
414
671
  fragmentNode: ReaderFragment,
415
672
  fragmentOwner: RequestDescriptor,
416
- ): Promise<void> | null {
417
- const environment = this._environment;
418
- const networkPromise =
419
- getPromiseForActiveRequest(environment, fragmentOwner) ??
420
- getPromiseForPendingOperationAffectingOwner(environment, fragmentOwner);
421
-
422
- 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) {
423
684
  return null;
424
685
  }
686
+
425
687
  // When the Promise for the request resolves, we need to make sure to
426
688
  // update the cache with the latest data available in the store before
427
689
  // resolving the Promise
690
+ const networkPromise = pendingOperationsResult.promise;
691
+ const pendingOperations = pendingOperationsResult.pendingOperations;
428
692
  const promise = networkPromise
429
693
  .then(() => {
430
694
  this._cache.delete(cacheKey);
@@ -432,17 +696,15 @@ class FragmentResourceImpl {
432
696
  .catch((error: Error) => {
433
697
  this._cache.delete(cacheKey);
434
698
  });
435
- this._cache.set(cacheKey, promise);
436
-
437
- const queryName = fragmentOwner.node.params.name;
438
- const fragmentName = fragmentNode.name;
439
- const promiseDisplayName =
440
- queryName === fragmentName
441
- ? `Relay(${queryName})`
442
- : `Relay(${queryName}:${fragmentName})`;
443
699
  // $FlowExpectedError[prop-missing] Expando to annotate Promises.
444
- promise.displayName = promiseDisplayName;
445
- 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};
446
708
  }
447
709
 
448
710
  _updatePluralSnapshot(
@@ -450,6 +712,7 @@ class FragmentResourceImpl {
450
712
  baseSnapshots: $ReadOnlyArray<Snapshot>,
451
713
  latestSnapshot: Snapshot,
452
714
  idx: number,
715
+ storeEpoch: number,
453
716
  ): void {
454
717
  const currentFragmentResult = this._cache.get(cacheKey);
455
718
  if (isPromise(currentFragmentResult)) {
@@ -457,7 +720,7 @@ class FragmentResourceImpl {
457
720
  return;
458
721
  }
459
722
 
460
- const currentSnapshot = currentFragmentResult?.snapshot;
723
+ const currentSnapshot = currentFragmentResult?.result?.snapshot;
461
724
  if (currentSnapshot && !Array.isArray(currentSnapshot)) {
462
725
  reportInvalidCachedData(latestSnapshot.selector.node.name);
463
726
  return;
@@ -467,7 +730,10 @@ class FragmentResourceImpl {
467
730
  ? [...currentSnapshot]
468
731
  : [...baseSnapshots];
469
732
  nextSnapshots[idx] = latestSnapshot;
470
- this._cache.set(cacheKey, getFragmentResult(cacheKey, nextSnapshots));
733
+ this._cache.set(cacheKey, {
734
+ kind: 'done',
735
+ result: getFragmentResult(cacheKey, nextSnapshots, storeEpoch),
736
+ });
471
737
  }
472
738
  }
473
739
 
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Copyright (c) Facebook, Inc. and its affiliates.
2
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
3
3
  *
4
4
  * This source code is licensed under the MIT license found in the
5
5
  * LICENSE file in the root directory of this source tree.
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Copyright (c) Facebook, Inc. and its affiliates.
2
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
3
3
  *
4
4
  * This source code is licensed under the MIT license found in the
5
5
  * LICENSE file in the root directory of this source tree.