react-relay 11.0.0 → 13.0.0-rc.0

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