react-relay 13.2.0 → 14.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (62) hide show
  1. package/ReactRelayContext.js +1 -1
  2. package/ReactRelayFragmentContainer.js.flow +7 -4
  3. package/ReactRelayPaginationContainer.js.flow +13 -8
  4. package/ReactRelayQueryFetcher.js.flow +1 -0
  5. package/ReactRelayQueryRenderer.js.flow +6 -2
  6. package/ReactRelayRefetchContainer.js.flow +10 -3
  7. package/__flowtests__/__generated__/ReactRelayFragmentContainerFlowtest_viewer.graphql.js.flow +2 -2
  8. package/__flowtests__/__generated__/ReactRelayFragmentContainerFlowtest_viewer2.graphql.js.flow +2 -2
  9. package/__flowtests__/__generated__/ReactRelayPaginationContainerFlowtestQuery.graphql.js.flow +3 -3
  10. package/__flowtests__/__generated__/ReactRelayPaginationContainerFlowtest_viewer.graphql.js.flow +3 -3
  11. package/__flowtests__/__generated__/ReactRelayRefetchContainerFlowtestQuery.graphql.js.flow +3 -3
  12. package/__flowtests__/__generated__/ReactRelayRefetchContainerFlowtest_viewer.graphql.js.flow +3 -3
  13. package/__flowtests__/__generated__/RelayModernFlowtest_badref.graphql.js.flow +2 -2
  14. package/__flowtests__/__generated__/RelayModernFlowtest_notref.graphql.js.flow +2 -2
  15. package/__flowtests__/__generated__/RelayModernFlowtest_user.graphql.js.flow +2 -2
  16. package/__flowtests__/__generated__/RelayModernFlowtest_users.graphql.js.flow +2 -2
  17. package/buildReactRelayContainer.js.flow +2 -2
  18. package/hooks.js +1 -1
  19. package/index.js +1 -1
  20. package/legacy.js +1 -1
  21. package/lib/ReactRelayQueryFetcher.js +1 -0
  22. package/lib/ReactRelayQueryRenderer.js +0 -1
  23. package/lib/readContext.js +2 -1
  24. package/lib/relay-hooks/FragmentResource.js +52 -10
  25. package/lib/relay-hooks/HooksImplementation.js +29 -0
  26. package/lib/relay-hooks/MatchContainer.js +1 -0
  27. package/lib/relay-hooks/QueryResource.js +2 -1
  28. package/lib/relay-hooks/react-cache/getQueryResultOrFetchQuery_REACT_CACHE.js +203 -56
  29. package/lib/relay-hooks/react-cache/useFragmentInternal_REACT_CACHE.js +254 -109
  30. package/lib/relay-hooks/react-cache/useFragment_REACT_CACHE.js +51 -0
  31. package/lib/relay-hooks/react-cache/useLazyLoadQuery_REACT_CACHE.js +13 -2
  32. package/lib/relay-hooks/react-cache/usePreloadedQuery_REACT_CACHE.js +125 -0
  33. package/lib/relay-hooks/useFragment.js +15 -1
  34. package/lib/relay-hooks/useLazyLoadQuery.js +18 -2
  35. package/lib/relay-hooks/useMutation.js +4 -5
  36. package/lib/relay-hooks/usePreloadedQuery.js +18 -2
  37. package/package.json +2 -2
  38. package/react-relay-hooks.js +2 -2
  39. package/react-relay-hooks.min.js +2 -2
  40. package/react-relay-legacy.js +2 -2
  41. package/react-relay-legacy.min.js +2 -2
  42. package/react-relay.js +2 -2
  43. package/react-relay.min.js +2 -2
  44. package/readContext.js.flow +1 -0
  45. package/relay-hooks/FragmentResource.js.flow +55 -9
  46. package/relay-hooks/HooksImplementation.js.flow +45 -0
  47. package/relay-hooks/MatchContainer.js.flow +8 -1
  48. package/relay-hooks/QueryResource.js.flow +4 -2
  49. package/relay-hooks/__flowtests__/__generated__/useFragmentFlowtest_user.graphql.js.flow +2 -2
  50. package/relay-hooks/__flowtests__/__generated__/useFragmentFlowtest_users.graphql.js.flow +2 -2
  51. package/relay-hooks/loadQuery.js.flow +2 -1
  52. package/relay-hooks/react-cache/getQueryResultOrFetchQuery_REACT_CACHE.js.flow +245 -64
  53. package/relay-hooks/react-cache/useFragmentInternal_REACT_CACHE.js.flow +242 -99
  54. package/relay-hooks/react-cache/useFragment_REACT_CACHE.js.flow +74 -0
  55. package/relay-hooks/react-cache/useLazyLoadQuery_REACT_CACHE.js.flow +10 -4
  56. package/relay-hooks/react-cache/usePreloadedQuery_REACT_CACHE.js.flow +153 -0
  57. package/relay-hooks/useFragment.js.flow +17 -10
  58. package/relay-hooks/useLazyLoadQuery.js.flow +38 -3
  59. package/relay-hooks/useMutation.js.flow +3 -3
  60. package/relay-hooks/usePreloadedQuery.js.flow +30 -2
  61. package/relay-hooks/useRefetchableFragmentNode.js.flow +26 -11
  62. package/relay-hooks/useSubscription.js.flow +14 -8
@@ -15,19 +15,25 @@
15
15
 
16
16
  import type {
17
17
  FetchPolicy,
18
+ GraphQLResponse,
18
19
  IEnvironment,
20
+ Observable,
19
21
  OperationDescriptor,
20
22
  ReaderFragment,
21
23
  RenderPolicy,
22
24
  } from 'relay-runtime';
23
25
 
26
+ const SuspenseResource = require('../SuspenseResource');
24
27
  const {getCacheForType, getCacheSignal} = require('./RelayReactCache');
25
28
  const invariant = require('invariant');
26
29
  const {
30
+ RelayFeatureFlags,
27
31
  __internal: {fetchQuery: fetchQueryInternal},
28
32
  } = require('relay-runtime');
29
33
  const warning = require('warning');
30
34
 
35
+ type QueryCacheCommitable = () => () => void;
36
+
31
37
  type QueryResult = {|
32
38
  fragmentNode: ReaderFragment,
33
39
  fragmentRef: mixed,
@@ -36,26 +42,96 @@ type QueryResult = {|
36
42
  // Note that the status of a cache entry will be 'resolved' when partial
37
43
  // rendering is allowed, even if a fetch is ongoing. The pending status
38
44
  // is specifically to indicate that we should suspend.
39
- type QueryCacheEntry =
40
- | {|status: 'resolved', result: QueryResult|}
41
- | {|status: 'pending', promise: Promise<void>|}
42
- | {|status: 'rejected', error: Error|};
45
+ // Note also that the retainCount is different from the retain count of
46
+ // an operation, which is maintained by the Environment. This retain
47
+ // count is used in Legacy Timeouts mode to count how many components
48
+ // are mounted that use the entry, plus one count for the temporary retain
49
+ // before any components have mounted. It is unused when Legacy Timeouts
50
+ // mode is off.
51
+ type QueryCacheEntryStatus =
52
+ | {|
53
+ status: 'resolved',
54
+ result: QueryResult,
55
+ |}
56
+ | {|
57
+ status: 'pending',
58
+ promise: Promise<void>,
59
+ |}
60
+ | {|
61
+ status: 'rejected',
62
+ error: Error,
63
+ |};
43
64
 
44
- type QueryCache = Map<string, QueryCacheEntry>;
65
+ type QueryCacheEntry = {|
66
+ ...QueryCacheEntryStatus,
67
+ onCommit: QueryCacheCommitable,
68
+ suspenseResource: SuspenseResource | null,
69
+ |};
45
70
 
46
71
  const DEFAULT_FETCH_POLICY = 'store-or-network';
47
72
 
73
+ type QueryCacheKey = string;
74
+
75
+ class QueryCache {
76
+ _map: Map<IEnvironment, Map<QueryCacheKey, QueryCacheEntry>>;
77
+
78
+ constructor() {
79
+ this._map = new Map();
80
+ }
81
+
82
+ get(environment: IEnvironment, key: QueryCacheKey): QueryCacheEntry | void {
83
+ let forEnv = this._map.get(environment);
84
+ if (!forEnv) {
85
+ forEnv = new Map();
86
+ this._map.set(environment, forEnv);
87
+ }
88
+ return forEnv.get(key);
89
+ }
90
+
91
+ set(
92
+ environment: IEnvironment,
93
+ key: QueryCacheKey,
94
+ value: QueryCacheEntry,
95
+ ): void {
96
+ let forEnv = this._map.get(environment);
97
+ if (!forEnv) {
98
+ forEnv = new Map();
99
+ this._map.set(environment, forEnv);
100
+ }
101
+ forEnv.set(key, value);
102
+ }
103
+
104
+ delete(environment: IEnvironment, key: QueryCacheKey): void {
105
+ const forEnv = this._map.get(environment);
106
+ if (!forEnv) {
107
+ return;
108
+ }
109
+ forEnv.delete(key);
110
+ if (forEnv.size === 0) {
111
+ this._map.delete(environment);
112
+ }
113
+ }
114
+ }
115
+
48
116
  function createQueryCache(): QueryCache {
49
- return new Map();
117
+ return new QueryCache();
50
118
  }
51
119
 
120
+ const noopOnCommit = () => {
121
+ return () => undefined;
122
+ };
123
+
124
+ const noopPromise = new Promise(() => {});
125
+
52
126
  function getQueryCacheKey(
53
127
  operation: OperationDescriptor,
54
128
  fetchPolicy: FetchPolicy,
55
129
  renderPolicy: RenderPolicy,
56
- ): string {
57
- const cacheIdentifier = `${fetchPolicy}-${renderPolicy}-${operation.request.identifier}`;
58
- return cacheIdentifier;
130
+ fetchKey?: ?string | ?number,
131
+ ): QueryCacheKey {
132
+ return `${fetchPolicy}-${renderPolicy}-${operation.request.identifier}-${
133
+ fetchKey ?? ''
134
+ }`;
59
135
  }
60
136
 
61
137
  function constructQueryResult(operation: OperationDescriptor): QueryResult {
@@ -72,14 +148,28 @@ function constructQueryResult(operation: OperationDescriptor): QueryResult {
72
148
  };
73
149
  }
74
150
 
151
+ function makeInitialCacheEntry() {
152
+ return {
153
+ status: 'pending',
154
+ promise: noopPromise,
155
+ onCommit: noopOnCommit,
156
+ suspenseResource: null,
157
+ };
158
+ }
159
+
75
160
  function getQueryResultOrFetchQuery_REACT_CACHE(
76
161
  environment: IEnvironment,
77
162
  queryOperationDescriptor: OperationDescriptor,
78
- fetchPolicy: FetchPolicy = DEFAULT_FETCH_POLICY,
79
- maybeRenderPolicy?: RenderPolicy,
80
- ): QueryResult {
163
+ options?: {|
164
+ fetchPolicy?: FetchPolicy,
165
+ renderPolicy?: RenderPolicy,
166
+ fetchKey?: ?string | ?number,
167
+ fetchObservable?: Observable<GraphQLResponse>,
168
+ |},
169
+ ): [QueryResult, QueryCacheCommitable] {
170
+ const fetchPolicy = options?.fetchPolicy ?? DEFAULT_FETCH_POLICY;
81
171
  const renderPolicy =
82
- maybeRenderPolicy ?? environment.UNSTABLE_getDefaultRenderPolicy();
172
+ options?.renderPolicy ?? environment.UNSTABLE_getDefaultRenderPolicy();
83
173
 
84
174
  const cache = getCacheForType(createQueryCache);
85
175
 
@@ -87,43 +177,111 @@ function getQueryResultOrFetchQuery_REACT_CACHE(
87
177
  queryOperationDescriptor,
88
178
  fetchPolicy,
89
179
  renderPolicy,
180
+ options?.fetchKey,
90
181
  );
91
182
 
92
- let entry = cache.get(cacheKey);
93
- if (entry === undefined) {
94
- // Initiate a query to fetch the data if needed:
95
- entry = onCacheMiss(
96
- environment,
97
- queryOperationDescriptor,
98
- fetchPolicy,
99
- renderPolicy,
100
- newCacheEntry => {
101
- cache.set(cacheKey, newCacheEntry);
102
- },
103
- );
104
- cache.set(cacheKey, entry);
105
-
106
- // Since this is the first time rendering, retain the query. React will
107
- // trigger the abort signal when this cache entry is no longer needed.
108
- const retention = environment.retain(queryOperationDescriptor);
109
- const abortSignal = getCacheSignal();
110
- abortSignal.addEventListener(
111
- 'abort',
112
- () => {
183
+ const initialEntry = cache.get(environment, cacheKey);
184
+
185
+ function updateCache(
186
+ updater: QueryCacheEntryStatus => QueryCacheEntryStatus,
187
+ ) {
188
+ let currentEntry = cache.get(environment, cacheKey);
189
+ if (!currentEntry) {
190
+ currentEntry = makeInitialCacheEntry();
191
+ cache.set(environment, cacheKey, currentEntry);
192
+ }
193
+ // $FlowExpectedError[prop-missing] Extra properties are passed in -- this is fine
194
+ const newStatus: {...} = updater(currentEntry);
195
+ // $FlowExpectedError[cannot-spread-inexact] Flow cannot understand that this is valid...
196
+ cache.set(environment, cacheKey, {...currentEntry, ...newStatus});
197
+ // ... but we can because QueryCacheEntry spreads QueryCacheEntryStatus, so spreading
198
+ // a QueryCacheEntryStatus into a QueryCacheEntry will result in a valid QueryCacheEntry.
199
+ }
200
+
201
+ // Initiate a query to fetch the data if needed:
202
+ if (RelayFeatureFlags.USE_REACT_CACHE_LEGACY_TIMEOUTS) {
203
+ let entry;
204
+ if (initialEntry === undefined) {
205
+ onCacheMiss(
206
+ environment,
207
+ queryOperationDescriptor,
208
+ fetchPolicy,
209
+ renderPolicy,
210
+ updateCache,
211
+ options?.fetchObservable,
212
+ );
213
+ const createdEntry = cache.get(environment, cacheKey);
214
+ invariant(
215
+ createdEntry !== undefined,
216
+ 'An entry should have been created by onCacheMiss. This is a bug in Relay.',
217
+ );
218
+ entry = createdEntry;
219
+ } else {
220
+ entry = initialEntry;
221
+ }
222
+ if (!entry.suspenseResource) {
223
+ entry.suspenseResource = new SuspenseResource(() => {
224
+ const retention = environment.retain(queryOperationDescriptor);
225
+ return {
226
+ dispose: () => {
227
+ retention.dispose();
228
+ cache.delete(environment, cacheKey);
229
+ },
230
+ };
231
+ });
232
+ }
233
+ if (entry.onCommit === noopOnCommit) {
234
+ entry.onCommit = () => {
235
+ invariant(
236
+ entry.suspenseResource,
237
+ 'SuspenseResource should have been initialized. This is a bug in Relay.',
238
+ );
239
+ const retention = entry.suspenseResource.permanentRetain(environment);
240
+ return () => {
241
+ retention.dispose();
242
+ };
243
+ };
244
+ }
245
+ entry.suspenseResource.temporaryRetain(environment);
246
+ } else {
247
+ if (initialEntry === undefined) {
248
+ // This is the behavior we eventually want: We retain the query until the
249
+ // presiding Cache component unmounts, at which point the AbortSignal
250
+ // will be triggered.
251
+ onCacheMiss(
252
+ environment,
253
+ queryOperationDescriptor,
254
+ fetchPolicy,
255
+ renderPolicy,
256
+ updateCache,
257
+ options?.fetchObservable,
258
+ );
259
+
260
+ // Since this is the first time rendering, retain the query. React will
261
+ // trigger the abort signal when this cache entry is no longer needed.
262
+ const retention = environment.retain(queryOperationDescriptor);
263
+
264
+ const dispose = () => {
113
265
  retention.dispose();
114
- cache.delete(cacheKey);
115
- },
116
- {once: true},
117
- );
266
+ cache.delete(environment, cacheKey);
267
+ };
268
+ const abortSignal = getCacheSignal();
269
+ abortSignal.addEventListener('abort', dispose, {once: true});
270
+ }
118
271
  }
119
272
 
273
+ const entry = cache.get(environment, cacheKey); // could be a different entry now if synchronously resolved
274
+ invariant(
275
+ entry !== undefined,
276
+ 'An entry should have been created by onCacheMiss. This is a bug in Relay.',
277
+ );
120
278
  switch (entry.status) {
121
279
  case 'pending':
122
280
  throw entry.promise;
123
281
  case 'rejected':
124
282
  throw entry.error;
125
283
  case 'resolved':
126
- return entry.result;
284
+ return [entry.result, entry.onCommit];
127
285
  }
128
286
  invariant(false, 'switch statement should be exhaustive');
129
287
  }
@@ -133,8 +291,9 @@ function onCacheMiss(
133
291
  operation: OperationDescriptor,
134
292
  fetchPolicy: FetchPolicy,
135
293
  renderPolicy: RenderPolicy,
136
- updateCache: QueryCacheEntry => void,
137
- ): QueryCacheEntry {
294
+ updateCache: ((QueryCacheEntryStatus) => QueryCacheEntryStatus) => void,
295
+ customFetchObservable?: Observable<GraphQLResponse>,
296
+ ): void {
138
297
  // NB: Besides checking if the data is available, calling `check` will write missing
139
298
  // data to the store using any missing data handlers specified in the environment.
140
299
  const queryAvailability = environment.check(operation);
@@ -169,25 +328,46 @@ function onCacheMiss(
169
328
  }
170
329
  }
171
330
 
172
- const promise = shouldFetch
173
- ? executeOperationAndKeepUpToDate(environment, operation, updateCache)
174
- : undefined;
175
- if (shouldRenderNow) {
176
- return {status: 'resolved', result: constructQueryResult(operation)};
331
+ if (shouldFetch) {
332
+ executeOperationAndKeepUpToDate(
333
+ environment,
334
+ operation,
335
+ updateCache,
336
+ customFetchObservable,
337
+ );
338
+ updateCache(existing => {
339
+ switch (existing.status) {
340
+ case 'resolved':
341
+ return existing;
342
+ case 'rejected':
343
+ return existing;
344
+ case 'pending':
345
+ return shouldRenderNow
346
+ ? {
347
+ status: 'resolved',
348
+ result: constructQueryResult(operation),
349
+ }
350
+ : existing;
351
+ }
352
+ });
177
353
  } else {
178
354
  invariant(
179
- promise,
180
- 'Should either fetch or render (or both), otherwise we would suspend forever.',
355
+ shouldRenderNow,
356
+ 'Should either fetch or be willing to render. This is a bug in Relay.',
181
357
  );
182
- return {status: 'pending', promise: promise};
358
+ updateCache(_existing => ({
359
+ status: 'resolved',
360
+ result: constructQueryResult(operation),
361
+ }));
183
362
  }
184
363
  }
185
364
 
186
365
  function executeOperationAndKeepUpToDate(
187
366
  environment: IEnvironment,
188
367
  operation: OperationDescriptor,
189
- updateCache: QueryCacheEntry => void,
190
- ): Promise<void> {
368
+ updateCache: ((QueryCacheEntryStatus) => QueryCacheEntryStatus) => void,
369
+ customFetchObservable?: Observable<GraphQLResponse>,
370
+ ) {
191
371
  let resolvePromise;
192
372
  const promise = new Promise(r => {
193
373
  resolvePromise = r;
@@ -198,12 +378,16 @@ function executeOperationAndKeepUpToDate(
198
378
  let isFirstPayload = true;
199
379
 
200
380
  // FIXME We may still need to cancel network requests for live queries.
201
- const fetchObservable = fetchQueryInternal(environment, operation);
381
+ const fetchObservable =
382
+ customFetchObservable ?? fetchQueryInternal(environment, operation);
202
383
  fetchObservable.subscribe({
203
384
  start: subscription => {},
204
385
  error: error => {
205
386
  if (isFirstPayload) {
206
- updateCache({status: 'rejected', error});
387
+ updateCache(_existing => ({
388
+ status: 'rejected',
389
+ error,
390
+ }));
207
391
  } else {
208
392
  // TODO:T92030819 Remove this warning and actually throw the network error
209
393
  // To complete this task we need to have a way of precisely tracking suspendable points
@@ -220,24 +404,21 @@ function executeOperationAndKeepUpToDate(
220
404
  },
221
405
  next: response => {
222
406
  // Stop suspending on the first payload because of streaming, defer, etc.
223
- updateCache({
224
- status: 'resolved',
225
- result: constructQueryResult(operation),
226
- });
227
- resolvePromise();
228
- isFirstPayload = false;
229
- },
230
- complete: () => {
231
- updateCache({
407
+ updateCache(_existing => ({
232
408
  status: 'resolved',
233
409
  result: constructQueryResult(operation),
234
- });
410
+ }));
235
411
  resolvePromise();
236
412
  isFirstPayload = false;
237
413
  },
238
414
  });
239
415
 
240
- return promise;
416
+ // If the above subscription yields a value synchronously, then one of the updates
417
+ // above will have already happened and we'll now be in a resolved or rejected state.
418
+ // But in the usual case, we save the promise to the entry here:
419
+ updateCache(existing =>
420
+ existing.status === 'pending' ? {status: 'pending', promise} : existing,
421
+ );
241
422
  }
242
423
 
243
424
  module.exports = getQueryResultOrFetchQuery_REACT_CACHE;