react-relay 13.1.1 → 13.2.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 (30) hide show
  1. package/ReactRelayContext.js +1 -1
  2. package/ReactRelayLocalQueryRenderer.js.flow +1 -1
  3. package/ReactRelayQueryRenderer.js.flow +1 -4
  4. package/hooks.js +1 -1
  5. package/index.js +1 -1
  6. package/jest-react/internalAct.js.flow +25 -9
  7. package/legacy.js +1 -1
  8. package/lib/ReactRelayQueryRenderer.js +1 -1
  9. package/lib/jest-react/internalAct.js +24 -4
  10. package/lib/relay-hooks/FragmentResource.js +10 -13
  11. package/lib/relay-hooks/QueryResource.js +2 -165
  12. package/lib/relay-hooks/preloadQuery_DEPRECATED.js +7 -11
  13. package/lib/relay-hooks/react-cache/RelayReactCache.js +37 -0
  14. package/lib/relay-hooks/react-cache/getQueryResultOrFetchQuery_REACT_CACHE.js +197 -0
  15. package/lib/relay-hooks/react-cache/useFragmentInternal_REACT_CACHE.js +395 -0
  16. package/lib/relay-hooks/react-cache/useLazyLoadQuery_REACT_CACHE.js +45 -0
  17. package/package.json +3 -3
  18. package/react-relay-hooks.js +2 -2
  19. package/react-relay-hooks.min.js +2 -2
  20. package/react-relay-legacy.js +2 -2
  21. package/react-relay-legacy.min.js +2 -2
  22. package/react-relay.js +2 -2
  23. package/react-relay.min.js +2 -2
  24. package/relay-hooks/FragmentResource.js.flow +17 -18
  25. package/relay-hooks/QueryResource.js.flow +4 -201
  26. package/relay-hooks/preloadQuery_DEPRECATED.js.flow +7 -14
  27. package/relay-hooks/react-cache/RelayReactCache.js.flow +42 -0
  28. package/relay-hooks/react-cache/getQueryResultOrFetchQuery_REACT_CACHE.js.flow +243 -0
  29. package/relay-hooks/react-cache/useFragmentInternal_REACT_CACHE.js.flow +416 -0
  30. package/relay-hooks/react-cache/useLazyLoadQuery_REACT_CACHE.js.flow +66 -0
@@ -32,7 +32,7 @@ import type {
32
32
  const LRUCache = require('./LRUCache');
33
33
  const SuspenseResource = require('./SuspenseResource');
34
34
  const invariant = require('invariant');
35
- const {RelayFeatureFlags, isPromise} = require('relay-runtime');
35
+ const {isPromise} = require('relay-runtime');
36
36
  const warning = require('warning');
37
37
 
38
38
  const CACHE_CAPACITY = 1000;
@@ -50,8 +50,6 @@ type QueryResourceCacheEntry = {|
50
50
  // from the incremental responses, so later we can choose how to handle errors
51
51
  // in the incremental payloads.
52
52
  processedPayloadsCount: number,
53
- getRetainCount(): number,
54
- getNetworkSubscription(): ?Subscription,
55
53
  setNetworkSubscription(?Subscription): void,
56
54
  getValue(): Error | Promise<void> | QueryResult,
57
55
  setValue(Error | Promise<void> | QueryResult): void,
@@ -125,39 +123,6 @@ function createCacheEntry(
125
123
  value: Error | Promise<void> | QueryResult,
126
124
  networkSubscription: ?Subscription,
127
125
  onDispose: QueryResourceCacheEntry => void,
128
- ): QueryResourceCacheEntry {
129
- // There should be no behavior difference between createCacheEntry_new and
130
- // createCacheEntry_old, and it doesn't directly relate to Client Edges.
131
- // It was just a refactoring that was needed for Client Edges but that
132
- // is behind the feature flag just in case there is any accidental breakage.
133
- if (RelayFeatureFlags.REFACTOR_SUSPENSE_RESOURCE) {
134
- return createCacheEntry_new(
135
- cacheIdentifier,
136
- operation,
137
- operationAvailability,
138
- value,
139
- networkSubscription,
140
- onDispose,
141
- );
142
- } else {
143
- return createCacheEntry_old(
144
- cacheIdentifier,
145
- operation,
146
- operationAvailability,
147
- value,
148
- networkSubscription,
149
- onDispose,
150
- );
151
- }
152
- }
153
-
154
- function createCacheEntry_new(
155
- cacheIdentifier: string,
156
- operation: OperationDescriptor,
157
- operationAvailability: ?OperationAvailability,
158
- value: Error | Promise<void> | QueryResult,
159
- networkSubscription: ?Subscription,
160
- onDispose: QueryResourceCacheEntry => void,
161
126
  ): QueryResourceCacheEntry {
162
127
  const isLiveQuery = operationIsLiveQuery(operation);
163
128
 
@@ -191,12 +156,6 @@ function createCacheEntry_new(
191
156
  setValue(val: QueryResult | Promise<void> | Error) {
192
157
  currentValue = val;
193
158
  },
194
- getRetainCount() {
195
- return suspenseResource.getRetainCount();
196
- },
197
- getNetworkSubscription() {
198
- return currentNetworkSubscription;
199
- },
200
159
  setNetworkSubscription(subscription: ?Subscription) {
201
160
  if (isLiveQuery && currentNetworkSubscription != null) {
202
161
  currentNetworkSubscription.unsubscribe();
@@ -217,153 +176,6 @@ function createCacheEntry_new(
217
176
  return cacheEntry;
218
177
  }
219
178
 
220
- const DATA_RETENTION_TIMEOUT = 5 * 60 * 1000;
221
- function createCacheEntry_old(
222
- cacheIdentifier: string,
223
- operation: OperationDescriptor,
224
- operationAvailability: ?OperationAvailability,
225
- value: Error | Promise<void> | QueryResult,
226
- networkSubscription: ?Subscription,
227
- onDispose: QueryResourceCacheEntry => void,
228
- ): QueryResourceCacheEntry {
229
- const isLiveQuery = operationIsLiveQuery(operation);
230
-
231
- let currentValue: Error | Promise<void> | QueryResult = value;
232
- let retainCount = 0;
233
- let retainDisposable: ?Disposable = null;
234
- let releaseTemporaryRetain: ?() => void = null;
235
- let currentNetworkSubscription: ?Subscription = networkSubscription;
236
-
237
- const retain = (environment: IEnvironment) => {
238
- retainCount++;
239
- if (retainCount === 1) {
240
- retainDisposable = environment.retain(operation);
241
- }
242
- return {
243
- dispose: () => {
244
- retainCount = Math.max(0, retainCount - 1);
245
- if (retainCount === 0) {
246
- invariant(
247
- retainDisposable != null,
248
- 'Relay: Expected disposable to release query to be defined.' +
249
- "If you're seeing this, this is likely a bug in Relay.",
250
- );
251
- retainDisposable.dispose();
252
- retainDisposable = null;
253
- }
254
- onDispose(cacheEntry);
255
- },
256
- };
257
- };
258
-
259
- const cacheEntry = {
260
- cacheIdentifier,
261
- id: nextID++,
262
- processedPayloadsCount: 0,
263
- operationAvailability,
264
- getValue() {
265
- return currentValue;
266
- },
267
- setValue(val: QueryResult | Promise<void> | Error) {
268
- currentValue = val;
269
- },
270
- getRetainCount() {
271
- return retainCount;
272
- },
273
- getNetworkSubscription() {
274
- return currentNetworkSubscription;
275
- },
276
- setNetworkSubscription(subscription: ?Subscription) {
277
- if (isLiveQuery && currentNetworkSubscription != null) {
278
- currentNetworkSubscription.unsubscribe();
279
- }
280
- currentNetworkSubscription = subscription;
281
- },
282
- temporaryRetain(environment: IEnvironment): Disposable {
283
- // NOTE: If we're executing in a server environment, there's no need
284
- // to create temporary retains, since the component will never commit.
285
- if (environment.isServer()) {
286
- return {dispose: () => {}};
287
- }
288
-
289
- // NOTE: temporaryRetain is called during the render phase. However,
290
- // given that we can't tell if this render will eventually commit or not,
291
- // we create a timer to autodispose of this retain in case the associated
292
- // component never commits.
293
- // If the component /does/ commit, permanentRetain will clear this timeout
294
- // and permanently retain the data.
295
- const disposable = retain(environment);
296
- let releaseQueryTimeout = null;
297
- const localReleaseTemporaryRetain = () => {
298
- clearTimeout(releaseQueryTimeout);
299
- releaseQueryTimeout = null;
300
- releaseTemporaryRetain = null;
301
- disposable.dispose();
302
- // Normally if this entry never commits, the request would've ended by the
303
- // time this timeout expires and the temporary retain is released. However,
304
- // we need to do this for live queries which remain open indefinitely.
305
- if (
306
- isLiveQuery &&
307
- retainCount <= 0 &&
308
- currentNetworkSubscription != null
309
- ) {
310
- currentNetworkSubscription.unsubscribe();
311
- }
312
- };
313
- releaseQueryTimeout = setTimeout(
314
- localReleaseTemporaryRetain,
315
- DATA_RETENTION_TIMEOUT,
316
- );
317
-
318
- // NOTE: Since temporaryRetain can be called multiple times, we release
319
- // the previous temporary retain after we re-establish a new one, since
320
- // we only ever need a single temporary retain until the permanent retain is
321
- // established.
322
- // temporaryRetain may be called multiple times by React during the render
323
- // phase, as well as multiple times by other query components that are
324
- // rendering the same query/variables.
325
- if (releaseTemporaryRetain != null) {
326
- releaseTemporaryRetain();
327
- }
328
- releaseTemporaryRetain = localReleaseTemporaryRetain;
329
-
330
- return {
331
- dispose: () => {
332
- releaseTemporaryRetain && releaseTemporaryRetain();
333
- },
334
- };
335
- },
336
- permanentRetain(environment: IEnvironment): Disposable {
337
- const disposable = retain(environment);
338
- if (releaseTemporaryRetain != null) {
339
- releaseTemporaryRetain();
340
- releaseTemporaryRetain = null;
341
- }
342
-
343
- return {
344
- dispose: () => {
345
- disposable.dispose();
346
- if (
347
- isLiveQuery &&
348
- retainCount <= 0 &&
349
- currentNetworkSubscription != null
350
- ) {
351
- currentNetworkSubscription.unsubscribe();
352
- }
353
- },
354
- };
355
- },
356
- releaseTemporaryRetain() {
357
- if (releaseTemporaryRetain != null) {
358
- releaseTemporaryRetain();
359
- releaseTemporaryRetain = null;
360
- }
361
- },
362
- };
363
-
364
- return cacheEntry;
365
- }
366
-
367
179
  class QueryResourceImpl {
368
180
  _environment: IEnvironment;
369
181
  _cache: QueryResourceCache;
@@ -532,15 +344,7 @@ class QueryResourceImpl {
532
344
  }
533
345
 
534
346
  _clearCacheEntry = (cacheEntry: QueryResourceCacheEntry): void => {
535
- // The new code does this retainCount <= 0 check within SuspenseResource
536
- // before calling _clearCacheEntry, whereas with the old code we do it here.
537
- if (RelayFeatureFlags.REFACTOR_SUSPENSE_RESOURCE) {
538
- this._cache.delete(cacheEntry.cacheIdentifier);
539
- } else {
540
- if (cacheEntry.getRetainCount() <= 0) {
541
- this._cache.delete(cacheEntry.cacheIdentifier);
542
- }
543
- }
347
+ this._cache.delete(cacheEntry.cacheIdentifier);
544
348
  };
545
349
 
546
350
  _getOrCreateCacheEntry(
@@ -693,10 +497,9 @@ class QueryResourceImpl {
693
497
  // To complete this task we need to have a way of precisely tracking suspendable points
694
498
  warning(
695
499
  false,
696
- 'QueryResource: An incremental payload for query `%` returned an error: `%`:`%`.',
500
+ 'QueryResource: An incremental payload for query `%s` returned an error: `%s`.',
697
501
  operation.fragment.node.name,
698
- error.message,
699
- error.stack,
502
+ String(error.message),
700
503
  );
701
504
  }
702
505
  resolveNetworkPromise();
@@ -115,20 +115,13 @@ function preloadQuery<TQuery: OperationType, TEnvironmentProviderOptions>(
115
115
  if (environment.isServer()) {
116
116
  return;
117
117
  }
118
- if (
119
- RelayFeatureFlags.DELAY_CLEANUP_OF_PENDING_PRELOAD_QUERIES ===
120
- true
121
- ) {
122
- setTimeout(() => {
123
- // Clear the cache entry after the default timeout
124
- // null-check for Flow
125
- if (queryEntry != null) {
126
- cleanup(pendingQueries, queryEntry);
127
- }
128
- }, DEFAULT_PREFETCH_TIMEOUT);
129
- } else {
130
- cleanup(pendingQueries, queryEntry);
131
- }
118
+ setTimeout(() => {
119
+ // Clear the cache entry after the default timeout
120
+ // null-check for Flow
121
+ if (queryEntry != null) {
122
+ cleanup(pendingQueries, queryEntry);
123
+ }
124
+ }, DEFAULT_PREFETCH_TIMEOUT);
132
125
  };
133
126
  })
134
127
  : null;
@@ -0,0 +1,42 @@
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
+ const invariant = require('invariant');
17
+ // $FlowFixMe[prop-missing] These exist in experimental builds but aren't in React's types yet.
18
+ const {unstable_getCacheForType, unstable_getCacheSignal} = require('react');
19
+ const {RelayFeatureFlags} = require('relay-runtime');
20
+
21
+ function getCacheForType<T>(factory: () => T): T {
22
+ invariant(
23
+ typeof unstable_getCacheForType === 'function' &&
24
+ RelayFeatureFlags.USE_REACT_CACHE,
25
+ 'RelayReactCache.getCacheForType should only be called when the USE_REACT_CACHE feature flag is enabled and when on an experimental React build that supports it.',
26
+ );
27
+ return unstable_getCacheForType(factory);
28
+ }
29
+
30
+ function getCacheSignal(): AbortSignal {
31
+ invariant(
32
+ typeof unstable_getCacheSignal === 'function' &&
33
+ RelayFeatureFlags.USE_REACT_CACHE,
34
+ 'RelayReactCache.getCacheSignal should only be called when the USE_REACT_CACHE feature flag is enabled and when on an experimental React build that supports it.',
35
+ );
36
+ return unstable_getCacheSignal();
37
+ }
38
+
39
+ module.exports = {
40
+ getCacheForType,
41
+ getCacheSignal,
42
+ };
@@ -0,0 +1,243 @@
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
+ IEnvironment,
19
+ OperationDescriptor,
20
+ ReaderFragment,
21
+ RenderPolicy,
22
+ } from 'relay-runtime';
23
+
24
+ const {getCacheForType, getCacheSignal} = require('./RelayReactCache');
25
+ const invariant = require('invariant');
26
+ const {
27
+ __internal: {fetchQuery: fetchQueryInternal},
28
+ } = require('relay-runtime');
29
+ const warning = require('warning');
30
+
31
+ type QueryResult = {|
32
+ fragmentNode: ReaderFragment,
33
+ fragmentRef: mixed,
34
+ |};
35
+
36
+ // Note that the status of a cache entry will be 'resolved' when partial
37
+ // rendering is allowed, even if a fetch is ongoing. The pending status
38
+ // 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|};
43
+
44
+ type QueryCache = Map<string, QueryCacheEntry>;
45
+
46
+ const DEFAULT_FETCH_POLICY = 'store-or-network';
47
+
48
+ function createQueryCache(): QueryCache {
49
+ return new Map();
50
+ }
51
+
52
+ function getQueryCacheKey(
53
+ operation: OperationDescriptor,
54
+ fetchPolicy: FetchPolicy,
55
+ renderPolicy: RenderPolicy,
56
+ ): string {
57
+ const cacheIdentifier = `${fetchPolicy}-${renderPolicy}-${operation.request.identifier}`;
58
+ return cacheIdentifier;
59
+ }
60
+
61
+ function constructQueryResult(operation: OperationDescriptor): QueryResult {
62
+ const rootFragmentRef = {
63
+ __id: operation.fragment.dataID,
64
+ __fragments: {
65
+ [operation.fragment.node.name]: operation.request.variables,
66
+ },
67
+ __fragmentOwner: operation.request,
68
+ };
69
+ return {
70
+ fragmentNode: operation.request.node.fragment,
71
+ fragmentRef: rootFragmentRef,
72
+ };
73
+ }
74
+
75
+ function getQueryResultOrFetchQuery_REACT_CACHE(
76
+ environment: IEnvironment,
77
+ queryOperationDescriptor: OperationDescriptor,
78
+ fetchPolicy: FetchPolicy = DEFAULT_FETCH_POLICY,
79
+ maybeRenderPolicy?: RenderPolicy,
80
+ ): QueryResult {
81
+ const renderPolicy =
82
+ maybeRenderPolicy ?? environment.UNSTABLE_getDefaultRenderPolicy();
83
+
84
+ const cache = getCacheForType(createQueryCache);
85
+
86
+ const cacheKey = getQueryCacheKey(
87
+ queryOperationDescriptor,
88
+ fetchPolicy,
89
+ renderPolicy,
90
+ );
91
+
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
+ () => {
113
+ retention.dispose();
114
+ cache.delete(cacheKey);
115
+ },
116
+ {once: true},
117
+ );
118
+ }
119
+
120
+ switch (entry.status) {
121
+ case 'pending':
122
+ throw entry.promise;
123
+ case 'rejected':
124
+ throw entry.error;
125
+ case 'resolved':
126
+ return entry.result;
127
+ }
128
+ invariant(false, 'switch statement should be exhaustive');
129
+ }
130
+
131
+ function onCacheMiss(
132
+ environment: IEnvironment,
133
+ operation: OperationDescriptor,
134
+ fetchPolicy: FetchPolicy,
135
+ renderPolicy: RenderPolicy,
136
+ updateCache: QueryCacheEntry => void,
137
+ ): QueryCacheEntry {
138
+ // NB: Besides checking if the data is available, calling `check` will write missing
139
+ // data to the store using any missing data handlers specified in the environment.
140
+ const queryAvailability = environment.check(operation);
141
+ const queryStatus = queryAvailability.status;
142
+ const hasFullQuery = queryStatus === 'available';
143
+ const canPartialRender =
144
+ hasFullQuery || (renderPolicy === 'partial' && queryStatus !== 'stale');
145
+
146
+ let shouldFetch;
147
+ let shouldRenderNow;
148
+ switch (fetchPolicy) {
149
+ case 'store-only': {
150
+ shouldFetch = false;
151
+ shouldRenderNow = true;
152
+ break;
153
+ }
154
+ case 'store-or-network': {
155
+ shouldFetch = !hasFullQuery;
156
+ shouldRenderNow = canPartialRender;
157
+ break;
158
+ }
159
+ case 'store-and-network': {
160
+ shouldFetch = true;
161
+ shouldRenderNow = canPartialRender;
162
+ break;
163
+ }
164
+ case 'network-only':
165
+ default: {
166
+ shouldFetch = true;
167
+ shouldRenderNow = false;
168
+ break;
169
+ }
170
+ }
171
+
172
+ const promise = shouldFetch
173
+ ? executeOperationAndKeepUpToDate(environment, operation, updateCache)
174
+ : undefined;
175
+ if (shouldRenderNow) {
176
+ return {status: 'resolved', result: constructQueryResult(operation)};
177
+ } else {
178
+ invariant(
179
+ promise,
180
+ 'Should either fetch or render (or both), otherwise we would suspend forever.',
181
+ );
182
+ return {status: 'pending', promise: promise};
183
+ }
184
+ }
185
+
186
+ function executeOperationAndKeepUpToDate(
187
+ environment: IEnvironment,
188
+ operation: OperationDescriptor,
189
+ updateCache: QueryCacheEntry => void,
190
+ ): Promise<void> {
191
+ let resolvePromise;
192
+ const promise = new Promise(r => {
193
+ resolvePromise = r;
194
+ });
195
+ // $FlowExpectedError[prop-missing] Expando to annotate Promises.
196
+ promise.displayName = 'Relay(' + operation.request.node.operation.name + ')';
197
+
198
+ let isFirstPayload = true;
199
+
200
+ // FIXME We may still need to cancel network requests for live queries.
201
+ const fetchObservable = fetchQueryInternal(environment, operation);
202
+ fetchObservable.subscribe({
203
+ start: subscription => {},
204
+ error: error => {
205
+ if (isFirstPayload) {
206
+ updateCache({status: 'rejected', error});
207
+ } else {
208
+ // TODO:T92030819 Remove this warning and actually throw the network error
209
+ // To complete this task we need to have a way of precisely tracking suspendable points
210
+ warning(
211
+ false,
212
+ 'getQueryResultOrFetchQuery: An incremental payload for query `%` returned an error: `%`:`%`.',
213
+ operation.request.node.operation.name,
214
+ error.message,
215
+ error.stack,
216
+ );
217
+ }
218
+ resolvePromise();
219
+ isFirstPayload = false;
220
+ },
221
+ next: response => {
222
+ // 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({
232
+ status: 'resolved',
233
+ result: constructQueryResult(operation),
234
+ });
235
+ resolvePromise();
236
+ isFirstPayload = false;
237
+ },
238
+ });
239
+
240
+ return promise;
241
+ }
242
+
243
+ module.exports = getQueryResultOrFetchQuery_REACT_CACHE;