react-relay 11.0.2 → 13.0.0-rc.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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.