react-relay 13.2.0 → 14.0.0

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 (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;