react-relay 13.1.0 → 14.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (69) hide show
  1. package/ReactRelayContext.js +1 -1
  2. package/ReactRelayFragmentContainer.js.flow +7 -4
  3. package/ReactRelayLocalQueryRenderer.js.flow +1 -1
  4. package/ReactRelayPaginationContainer.js.flow +13 -8
  5. package/ReactRelayQueryFetcher.js.flow +1 -0
  6. package/ReactRelayQueryRenderer.js.flow +7 -6
  7. package/ReactRelayRefetchContainer.js.flow +10 -3
  8. package/__flowtests__/__generated__/ReactRelayFragmentContainerFlowtest_viewer.graphql.js.flow +2 -2
  9. package/__flowtests__/__generated__/ReactRelayFragmentContainerFlowtest_viewer2.graphql.js.flow +2 -2
  10. package/__flowtests__/__generated__/ReactRelayPaginationContainerFlowtestQuery.graphql.js.flow +3 -3
  11. package/__flowtests__/__generated__/ReactRelayPaginationContainerFlowtest_viewer.graphql.js.flow +3 -3
  12. package/__flowtests__/__generated__/ReactRelayRefetchContainerFlowtestQuery.graphql.js.flow +3 -3
  13. package/__flowtests__/__generated__/ReactRelayRefetchContainerFlowtest_viewer.graphql.js.flow +3 -3
  14. package/__flowtests__/__generated__/RelayModernFlowtest_badref.graphql.js.flow +2 -2
  15. package/__flowtests__/__generated__/RelayModernFlowtest_notref.graphql.js.flow +2 -2
  16. package/__flowtests__/__generated__/RelayModernFlowtest_user.graphql.js.flow +2 -2
  17. package/__flowtests__/__generated__/RelayModernFlowtest_users.graphql.js.flow +2 -2
  18. package/buildReactRelayContainer.js.flow +2 -2
  19. package/hooks.js +1 -1
  20. package/index.js +1 -1
  21. package/jest-react/internalAct.js.flow +25 -9
  22. package/legacy.js +1 -1
  23. package/lib/ReactRelayQueryFetcher.js +1 -0
  24. package/lib/ReactRelayQueryRenderer.js +1 -2
  25. package/lib/jest-react/internalAct.js +24 -4
  26. package/lib/readContext.js +2 -1
  27. package/lib/relay-hooks/FragmentResource.js +62 -23
  28. package/lib/relay-hooks/HooksImplementation.js +29 -0
  29. package/lib/relay-hooks/MatchContainer.js +1 -0
  30. package/lib/relay-hooks/QueryResource.js +4 -166
  31. package/lib/relay-hooks/preloadQuery_DEPRECATED.js +7 -11
  32. package/lib/relay-hooks/react-cache/RelayReactCache.js +37 -0
  33. package/lib/relay-hooks/react-cache/getQueryResultOrFetchQuery_REACT_CACHE.js +344 -0
  34. package/lib/relay-hooks/react-cache/useFragmentInternal_REACT_CACHE.js +540 -0
  35. package/lib/relay-hooks/react-cache/useFragment_REACT_CACHE.js +51 -0
  36. package/lib/relay-hooks/react-cache/useLazyLoadQuery_REACT_CACHE.js +56 -0
  37. package/lib/relay-hooks/react-cache/usePreloadedQuery_REACT_CACHE.js +125 -0
  38. package/lib/relay-hooks/useFragment.js +15 -1
  39. package/lib/relay-hooks/useLazyLoadQuery.js +18 -2
  40. package/lib/relay-hooks/useMutation.js +4 -5
  41. package/lib/relay-hooks/usePreloadedQuery.js +18 -2
  42. package/package.json +3 -3
  43. package/react-relay-hooks.js +2 -2
  44. package/react-relay-hooks.min.js +2 -2
  45. package/react-relay-legacy.js +2 -2
  46. package/react-relay-legacy.min.js +2 -2
  47. package/react-relay.js +2 -2
  48. package/react-relay.min.js +2 -2
  49. package/readContext.js.flow +1 -0
  50. package/relay-hooks/FragmentResource.js.flow +72 -27
  51. package/relay-hooks/HooksImplementation.js.flow +45 -0
  52. package/relay-hooks/MatchContainer.js.flow +8 -1
  53. package/relay-hooks/QueryResource.js.flow +8 -203
  54. package/relay-hooks/__flowtests__/__generated__/useFragmentFlowtest_user.graphql.js.flow +2 -2
  55. package/relay-hooks/__flowtests__/__generated__/useFragmentFlowtest_users.graphql.js.flow +2 -2
  56. package/relay-hooks/loadQuery.js.flow +2 -1
  57. package/relay-hooks/preloadQuery_DEPRECATED.js.flow +7 -14
  58. package/relay-hooks/react-cache/RelayReactCache.js.flow +42 -0
  59. package/relay-hooks/react-cache/getQueryResultOrFetchQuery_REACT_CACHE.js.flow +424 -0
  60. package/relay-hooks/react-cache/useFragmentInternal_REACT_CACHE.js.flow +559 -0
  61. package/relay-hooks/react-cache/useFragment_REACT_CACHE.js.flow +74 -0
  62. package/relay-hooks/react-cache/useLazyLoadQuery_REACT_CACHE.js.flow +72 -0
  63. package/relay-hooks/react-cache/usePreloadedQuery_REACT_CACHE.js.flow +153 -0
  64. package/relay-hooks/useFragment.js.flow +17 -10
  65. package/relay-hooks/useLazyLoadQuery.js.flow +38 -3
  66. package/relay-hooks/useMutation.js.flow +3 -3
  67. package/relay-hooks/usePreloadedQuery.js.flow +30 -2
  68. package/relay-hooks/useRefetchableFragmentNode.js.flow +26 -11
  69. package/relay-hooks/useSubscription.js.flow +14 -8
@@ -0,0 +1,424 @@
1
+ /**
2
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ * @flow strict-local
8
+ * @emails oncall+relay
9
+ * @format
10
+ */
11
+
12
+ // flowlint ambiguous-object-type:error
13
+
14
+ 'use strict';
15
+
16
+ import type {
17
+ FetchPolicy,
18
+ GraphQLResponse,
19
+ IEnvironment,
20
+ Observable,
21
+ OperationDescriptor,
22
+ ReaderFragment,
23
+ RenderPolicy,
24
+ } from 'relay-runtime';
25
+
26
+ const SuspenseResource = require('../SuspenseResource');
27
+ const {getCacheForType, getCacheSignal} = require('./RelayReactCache');
28
+ const invariant = require('invariant');
29
+ const {
30
+ RelayFeatureFlags,
31
+ __internal: {fetchQuery: fetchQueryInternal},
32
+ } = require('relay-runtime');
33
+ const warning = require('warning');
34
+
35
+ type QueryCacheCommitable = () => () => void;
36
+
37
+ type QueryResult = {|
38
+ fragmentNode: ReaderFragment,
39
+ fragmentRef: mixed,
40
+ |};
41
+
42
+ // Note that the status of a cache entry will be 'resolved' when partial
43
+ // rendering is allowed, even if a fetch is ongoing. The pending status
44
+ // is specifically to indicate that we should suspend.
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
+ |};
64
+
65
+ type QueryCacheEntry = {|
66
+ ...QueryCacheEntryStatus,
67
+ onCommit: QueryCacheCommitable,
68
+ suspenseResource: SuspenseResource | null,
69
+ |};
70
+
71
+ const DEFAULT_FETCH_POLICY = 'store-or-network';
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
+
116
+ function createQueryCache(): QueryCache {
117
+ return new QueryCache();
118
+ }
119
+
120
+ const noopOnCommit = () => {
121
+ return () => undefined;
122
+ };
123
+
124
+ const noopPromise = new Promise(() => {});
125
+
126
+ function getQueryCacheKey(
127
+ operation: OperationDescriptor,
128
+ fetchPolicy: FetchPolicy,
129
+ renderPolicy: RenderPolicy,
130
+ fetchKey?: ?string | ?number,
131
+ ): QueryCacheKey {
132
+ return `${fetchPolicy}-${renderPolicy}-${operation.request.identifier}-${
133
+ fetchKey ?? ''
134
+ }`;
135
+ }
136
+
137
+ function constructQueryResult(operation: OperationDescriptor): QueryResult {
138
+ const rootFragmentRef = {
139
+ __id: operation.fragment.dataID,
140
+ __fragments: {
141
+ [operation.fragment.node.name]: operation.request.variables,
142
+ },
143
+ __fragmentOwner: operation.request,
144
+ };
145
+ return {
146
+ fragmentNode: operation.request.node.fragment,
147
+ fragmentRef: rootFragmentRef,
148
+ };
149
+ }
150
+
151
+ function makeInitialCacheEntry() {
152
+ return {
153
+ status: 'pending',
154
+ promise: noopPromise,
155
+ onCommit: noopOnCommit,
156
+ suspenseResource: null,
157
+ };
158
+ }
159
+
160
+ function getQueryResultOrFetchQuery_REACT_CACHE(
161
+ environment: IEnvironment,
162
+ queryOperationDescriptor: OperationDescriptor,
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;
171
+ const renderPolicy =
172
+ options?.renderPolicy ?? environment.UNSTABLE_getDefaultRenderPolicy();
173
+
174
+ const cache = getCacheForType(createQueryCache);
175
+
176
+ const cacheKey = getQueryCacheKey(
177
+ queryOperationDescriptor,
178
+ fetchPolicy,
179
+ renderPolicy,
180
+ options?.fetchKey,
181
+ );
182
+
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 = () => {
265
+ retention.dispose();
266
+ cache.delete(environment, cacheKey);
267
+ };
268
+ const abortSignal = getCacheSignal();
269
+ abortSignal.addEventListener('abort', dispose, {once: true});
270
+ }
271
+ }
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
+ );
278
+ switch (entry.status) {
279
+ case 'pending':
280
+ throw entry.promise;
281
+ case 'rejected':
282
+ throw entry.error;
283
+ case 'resolved':
284
+ return [entry.result, entry.onCommit];
285
+ }
286
+ invariant(false, 'switch statement should be exhaustive');
287
+ }
288
+
289
+ function onCacheMiss(
290
+ environment: IEnvironment,
291
+ operation: OperationDescriptor,
292
+ fetchPolicy: FetchPolicy,
293
+ renderPolicy: RenderPolicy,
294
+ updateCache: ((QueryCacheEntryStatus) => QueryCacheEntryStatus) => void,
295
+ customFetchObservable?: Observable<GraphQLResponse>,
296
+ ): void {
297
+ // NB: Besides checking if the data is available, calling `check` will write missing
298
+ // data to the store using any missing data handlers specified in the environment.
299
+ const queryAvailability = environment.check(operation);
300
+ const queryStatus = queryAvailability.status;
301
+ const hasFullQuery = queryStatus === 'available';
302
+ const canPartialRender =
303
+ hasFullQuery || (renderPolicy === 'partial' && queryStatus !== 'stale');
304
+
305
+ let shouldFetch;
306
+ let shouldRenderNow;
307
+ switch (fetchPolicy) {
308
+ case 'store-only': {
309
+ shouldFetch = false;
310
+ shouldRenderNow = true;
311
+ break;
312
+ }
313
+ case 'store-or-network': {
314
+ shouldFetch = !hasFullQuery;
315
+ shouldRenderNow = canPartialRender;
316
+ break;
317
+ }
318
+ case 'store-and-network': {
319
+ shouldFetch = true;
320
+ shouldRenderNow = canPartialRender;
321
+ break;
322
+ }
323
+ case 'network-only':
324
+ default: {
325
+ shouldFetch = true;
326
+ shouldRenderNow = false;
327
+ break;
328
+ }
329
+ }
330
+
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
+ });
353
+ } else {
354
+ invariant(
355
+ shouldRenderNow,
356
+ 'Should either fetch or be willing to render. This is a bug in Relay.',
357
+ );
358
+ updateCache(_existing => ({
359
+ status: 'resolved',
360
+ result: constructQueryResult(operation),
361
+ }));
362
+ }
363
+ }
364
+
365
+ function executeOperationAndKeepUpToDate(
366
+ environment: IEnvironment,
367
+ operation: OperationDescriptor,
368
+ updateCache: ((QueryCacheEntryStatus) => QueryCacheEntryStatus) => void,
369
+ customFetchObservable?: Observable<GraphQLResponse>,
370
+ ) {
371
+ let resolvePromise;
372
+ const promise = new Promise(r => {
373
+ resolvePromise = r;
374
+ });
375
+ // $FlowExpectedError[prop-missing] Expando to annotate Promises.
376
+ promise.displayName = 'Relay(' + operation.request.node.operation.name + ')';
377
+
378
+ let isFirstPayload = true;
379
+
380
+ // FIXME We may still need to cancel network requests for live queries.
381
+ const fetchObservable =
382
+ customFetchObservable ?? fetchQueryInternal(environment, operation);
383
+ fetchObservable.subscribe({
384
+ start: subscription => {},
385
+ error: error => {
386
+ if (isFirstPayload) {
387
+ updateCache(_existing => ({
388
+ status: 'rejected',
389
+ error,
390
+ }));
391
+ } else {
392
+ // TODO:T92030819 Remove this warning and actually throw the network error
393
+ // To complete this task we need to have a way of precisely tracking suspendable points
394
+ warning(
395
+ false,
396
+ 'getQueryResultOrFetchQuery: An incremental payload for query `%` returned an error: `%`:`%`.',
397
+ operation.request.node.operation.name,
398
+ error.message,
399
+ error.stack,
400
+ );
401
+ }
402
+ resolvePromise();
403
+ isFirstPayload = false;
404
+ },
405
+ next: response => {
406
+ // Stop suspending on the first payload because of streaming, defer, etc.
407
+ updateCache(_existing => ({
408
+ status: 'resolved',
409
+ result: constructQueryResult(operation),
410
+ }));
411
+ resolvePromise();
412
+ isFirstPayload = false;
413
+ },
414
+ });
415
+
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
+ );
422
+ }
423
+
424
+ module.exports = getQueryResultOrFetchQuery_REACT_CACHE;