react-relay 0.0.0-main-f0fc1eea → 0.0.0-main-c454f22f
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.
- package/ReactRelayContext.js +1 -1
- package/__flowtests__/__generated__/ReactRelayPaginationContainerFlowtestQuery.graphql.js.flow +1 -2
- package/__flowtests__/__generated__/ReactRelayRefetchContainerFlowtestQuery.graphql.js.flow +1 -2
- package/__flowtests__/__generated__/RelayModernFlowtest_badref.graphql.js.flow +1 -2
- package/__flowtests__/__generated__/RelayModernFlowtest_notref.graphql.js.flow +1 -2
- package/hooks.js +1 -1
- package/index.js +1 -1
- package/legacy.js +1 -1
- package/lib/relay-hooks/FragmentResource.js +241 -45
- package/lib/relay-hooks/QueryResource.js +81 -2
- package/lib/relay-hooks/SuspenseResource.js +130 -0
- package/package.json +2 -2
- package/react-relay-hooks.js +2 -2
- package/react-relay-hooks.min.js +2 -2
- package/react-relay-legacy.js +1 -1
- package/react-relay-legacy.min.js +1 -1
- package/react-relay.js +2 -2
- package/react-relay.min.js +2 -2
- package/relay-hooks/FragmentResource.js.flow +202 -14
- package/relay-hooks/QueryResource.js.flow +110 -4
- package/relay-hooks/SuspenseResource.js.flow +115 -0
@@ -14,7 +14,10 @@
|
|
14
14
|
'use strict';
|
15
15
|
|
16
16
|
import type {Cache} from './LRUCache';
|
17
|
+
import type {QueryResource, QueryResult} from './QueryResource';
|
17
18
|
import type {
|
19
|
+
ConcreteRequest,
|
20
|
+
DataID,
|
18
21
|
Disposable,
|
19
22
|
IEnvironment,
|
20
23
|
ReaderFragment,
|
@@ -23,11 +26,17 @@ import type {
|
|
23
26
|
} from 'relay-runtime';
|
24
27
|
|
25
28
|
const LRUCache = require('./LRUCache');
|
29
|
+
const {getQueryResourceForEnvironment} = require('./QueryResource');
|
30
|
+
const SuspenseResource = require('./SuspenseResource');
|
26
31
|
const invariant = require('invariant');
|
27
32
|
const {
|
33
|
+
RelayFeatureFlags,
|
34
|
+
__internal: {fetchQuery, getPromiseForActiveRequest},
|
35
|
+
createOperationDescriptor,
|
28
36
|
getFragmentIdentifier,
|
29
37
|
getPendingOperationsForFragment,
|
30
38
|
getSelector,
|
39
|
+
getVariablesFromFragment,
|
31
40
|
isPromise,
|
32
41
|
recycleNodesInto,
|
33
42
|
reportMissingRequiredFields,
|
@@ -76,6 +85,24 @@ function isMissingData(snapshot: SingularOrPluralSnapshot): boolean {
|
|
76
85
|
return snapshot.isMissingData;
|
77
86
|
}
|
78
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
|
+
|
79
106
|
function getFragmentResult(
|
80
107
|
cacheKey: string,
|
81
108
|
snapshot: SingularOrPluralSnapshot,
|
@@ -99,13 +126,75 @@ function getFragmentResult(
|
|
99
126
|
};
|
100
127
|
}
|
101
128
|
|
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
|
+
}
|
183
|
+
}
|
184
|
+
|
102
185
|
class FragmentResourceImpl {
|
103
186
|
_environment: IEnvironment;
|
104
187
|
_cache: FragmentResourceCache;
|
188
|
+
_clientEdgeQueryResultsCache: void | ClientEdgeQueryResultsCache;
|
105
189
|
|
106
190
|
constructor(environment: IEnvironment) {
|
107
191
|
this._environment = environment;
|
108
192
|
this._cache = LRUCache.create(CACHE_CAPACITY);
|
193
|
+
if (RelayFeatureFlags.ENABLE_CLIENT_EDGES) {
|
194
|
+
this._clientEdgeQueryResultsCache = new ClientEdgeQueryResultsCache(
|
195
|
+
environment,
|
196
|
+
);
|
197
|
+
}
|
109
198
|
}
|
110
199
|
|
111
200
|
/**
|
@@ -250,25 +339,75 @@ class FragmentResourceImpl {
|
|
250
339
|
return fragmentResult;
|
251
340
|
}
|
252
341
|
|
253
|
-
// 3. If we don't have data in the store,
|
254
|
-
//
|
255
|
-
//
|
256
|
-
//
|
257
|
-
//
|
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:
|
258
395
|
const fragmentOwner =
|
259
396
|
fragmentSelector.kind === 'PluralReaderSelector'
|
260
397
|
? fragmentSelector.selectors[0].owner
|
261
398
|
: fragmentSelector.owner;
|
262
|
-
const
|
399
|
+
const parentQueryPromiseResult =
|
263
400
|
this._getAndSavePromiseForFragmentRequestInFlight(
|
264
401
|
fragmentIdentifier,
|
265
402
|
fragmentNode,
|
266
403
|
fragmentOwner,
|
267
404
|
fragmentResult,
|
268
405
|
);
|
406
|
+
const parentQueryPromiseResultPromise = parentQueryPromiseResult?.promise; // for refinement
|
407
|
+
|
269
408
|
if (
|
270
|
-
|
271
|
-
isPromise(
|
409
|
+
clientEdgePromises?.length ||
|
410
|
+
isPromise(parentQueryPromiseResultPromise)
|
272
411
|
) {
|
273
412
|
environment.__log({
|
274
413
|
name: 'suspense.fragment',
|
@@ -277,15 +416,52 @@ class FragmentResourceImpl {
|
|
277
416
|
isRelayHooks: true,
|
278
417
|
isPromiseCached: false,
|
279
418
|
isMissingData: fragmentResult.isMissingData,
|
280
|
-
pendingOperations:
|
419
|
+
pendingOperations: [
|
420
|
+
...(parentQueryPromiseResult?.pendingOperations ?? []),
|
421
|
+
...(clientEdgeRequests ?? []),
|
422
|
+
],
|
281
423
|
});
|
282
|
-
throw
|
424
|
+
throw clientEdgePromises?.length
|
425
|
+
? Promise.all([parentQueryPromiseResultPromise, ...clientEdgePromises])
|
426
|
+
: parentQueryPromiseResultPromise;
|
283
427
|
}
|
284
428
|
|
285
429
|
this._reportMissingRequiredFieldsInSnapshot(snapshot);
|
286
430
|
return getFragmentResult(fragmentIdentifier, snapshot, storeEpoch);
|
287
431
|
}
|
288
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
|
+
};
|
463
|
+
}
|
464
|
+
|
289
465
|
_reportMissingRequiredFieldsInSnapshot(snapshot: SingularOrPluralSnapshot) {
|
290
466
|
if (Array.isArray(snapshot)) {
|
291
467
|
snapshot.forEach(s => {
|
@@ -343,7 +519,7 @@ class FragmentResourceImpl {
|
|
343
519
|
}
|
344
520
|
|
345
521
|
// 3. Establish subscriptions on the snapshot(s)
|
346
|
-
const
|
522
|
+
const disposables = [];
|
347
523
|
if (Array.isArray(renderedSnapshot)) {
|
348
524
|
invariant(
|
349
525
|
Array.isArray(currentSnapshot),
|
@@ -351,7 +527,7 @@ class FragmentResourceImpl {
|
|
351
527
|
"If you're seeing this, this is likely a bug in Relay.",
|
352
528
|
);
|
353
529
|
currentSnapshot.forEach((snapshot, idx) => {
|
354
|
-
|
530
|
+
disposables.push(
|
355
531
|
environment.subscribe(snapshot, latestSnapshot => {
|
356
532
|
const storeEpoch = environment.getStore().getEpoch();
|
357
533
|
this._updatePluralSnapshot(
|
@@ -371,7 +547,7 @@ class FragmentResourceImpl {
|
|
371
547
|
'Relay: Expected snapshot to be singular. ' +
|
372
548
|
"If you're seeing this, this is likely a bug in Relay.",
|
373
549
|
);
|
374
|
-
|
550
|
+
disposables.push(
|
375
551
|
environment.subscribe(currentSnapshot, latestSnapshot => {
|
376
552
|
const storeEpoch = environment.getStore().getEpoch();
|
377
553
|
this._cache.set(cacheKey, {
|
@@ -383,9 +559,20 @@ class FragmentResourceImpl {
|
|
383
559
|
);
|
384
560
|
}
|
385
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
|
+
|
386
573
|
return {
|
387
574
|
dispose: () => {
|
388
|
-
|
575
|
+
disposables.forEach(s => s.dispose());
|
389
576
|
this._cache.delete(cacheKey);
|
390
577
|
},
|
391
578
|
};
|
@@ -456,6 +643,7 @@ class FragmentResourceImpl {
|
|
456
643
|
const updatedCurrentSnapshot: Snapshot = {
|
457
644
|
data: updatedData,
|
458
645
|
isMissingData: currentSnapshot.isMissingData,
|
646
|
+
missingClientEdges: currentSnapshot.missingClientEdges,
|
459
647
|
seenRecords: currentSnapshot.seenRecords,
|
460
648
|
selector: currentSnapshot.selector,
|
461
649
|
missingRequiredFields: currentSnapshot.missingRequiredFields,
|
@@ -30,13 +30,13 @@ import type {
|
|
30
30
|
} from 'relay-runtime';
|
31
31
|
|
32
32
|
const LRUCache = require('./LRUCache');
|
33
|
+
const SuspenseResource = require('./SuspenseResource');
|
33
34
|
const invariant = require('invariant');
|
34
|
-
const {isPromise} = require('relay-runtime');
|
35
|
+
const {RelayFeatureFlags, isPromise} = require('relay-runtime');
|
35
36
|
const warning = require('warning');
|
36
37
|
|
37
38
|
const CACHE_CAPACITY = 1000;
|
38
39
|
const DEFAULT_FETCH_POLICY = 'store-or-network';
|
39
|
-
const DATA_RETENTION_TIMEOUT = 5 * 60 * 1000;
|
40
40
|
|
41
41
|
export type QueryResource = QueryResourceImpl;
|
42
42
|
|
@@ -59,7 +59,7 @@ type QueryResourceCacheEntry = {|
|
|
59
59
|
permanentRetain(environment: IEnvironment): Disposable,
|
60
60
|
releaseTemporaryRetain(): void,
|
61
61
|
|};
|
62
|
-
opaque type QueryResult: {
|
62
|
+
export opaque type QueryResult: {
|
63
63
|
fragmentNode: ReaderFragment,
|
64
64
|
fragmentRef: mixed,
|
65
65
|
...
|
@@ -125,6 +125,106 @@ function createCacheEntry(
|
|
125
125
|
value: Error | Promise<void> | QueryResult,
|
126
126
|
networkSubscription: ?Subscription,
|
127
127
|
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
|
+
): QueryResourceCacheEntry {
|
162
|
+
const isLiveQuery = operationIsLiveQuery(operation);
|
163
|
+
|
164
|
+
let currentValue: Error | Promise<void> | QueryResult = value;
|
165
|
+
let currentNetworkSubscription: ?Subscription = networkSubscription;
|
166
|
+
|
167
|
+
const suspenseResource = new SuspenseResource(environment => {
|
168
|
+
const retention = environment.retain(operation);
|
169
|
+
return {
|
170
|
+
dispose: () => {
|
171
|
+
// Normally if this entry never commits, the request would've ended by the
|
172
|
+
// time this timeout expires and the temporary retain is released. However,
|
173
|
+
// we need to do this for live queries which remain open indefinitely.
|
174
|
+
if (isLiveQuery && currentNetworkSubscription != null) {
|
175
|
+
currentNetworkSubscription.unsubscribe();
|
176
|
+
}
|
177
|
+
retention.dispose();
|
178
|
+
onDispose(cacheEntry);
|
179
|
+
},
|
180
|
+
};
|
181
|
+
});
|
182
|
+
|
183
|
+
const cacheEntry = {
|
184
|
+
cacheIdentifier,
|
185
|
+
id: nextID++,
|
186
|
+
processedPayloadsCount: 0,
|
187
|
+
operationAvailability,
|
188
|
+
getValue() {
|
189
|
+
return currentValue;
|
190
|
+
},
|
191
|
+
setValue(val: QueryResult | Promise<void> | Error) {
|
192
|
+
currentValue = val;
|
193
|
+
},
|
194
|
+
getRetainCount() {
|
195
|
+
return suspenseResource.getRetainCount();
|
196
|
+
},
|
197
|
+
getNetworkSubscription() {
|
198
|
+
return currentNetworkSubscription;
|
199
|
+
},
|
200
|
+
setNetworkSubscription(subscription: ?Subscription) {
|
201
|
+
if (isLiveQuery && currentNetworkSubscription != null) {
|
202
|
+
currentNetworkSubscription.unsubscribe();
|
203
|
+
}
|
204
|
+
currentNetworkSubscription = subscription;
|
205
|
+
},
|
206
|
+
temporaryRetain(environment: IEnvironment): Disposable {
|
207
|
+
return suspenseResource.temporaryRetain(environment);
|
208
|
+
},
|
209
|
+
permanentRetain(environment: IEnvironment): Disposable {
|
210
|
+
return suspenseResource.permanentRetain(environment);
|
211
|
+
},
|
212
|
+
releaseTemporaryRetain() {
|
213
|
+
suspenseResource.releaseTemporaryRetain();
|
214
|
+
},
|
215
|
+
};
|
216
|
+
|
217
|
+
return cacheEntry;
|
218
|
+
}
|
219
|
+
|
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,
|
128
228
|
): QueryResourceCacheEntry {
|
129
229
|
const isLiveQuery = operationIsLiveQuery(operation);
|
130
230
|
|
@@ -432,8 +532,14 @@ class QueryResourceImpl {
|
|
432
532
|
}
|
433
533
|
|
434
534
|
_clearCacheEntry = (cacheEntry: QueryResourceCacheEntry): void => {
|
435
|
-
|
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) {
|
436
538
|
this._cache.delete(cacheEntry.cacheIdentifier);
|
539
|
+
} else {
|
540
|
+
if (cacheEntry.getRetainCount() <= 0) {
|
541
|
+
this._cache.delete(cacheEntry.cacheIdentifier);
|
542
|
+
}
|
437
543
|
}
|
438
544
|
};
|
439
545
|
|
@@ -0,0 +1,115 @@
|
|
1
|
+
/**
|
2
|
+
* Copyright (c) Facebook, Inc. and its 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 {Disposable, IEnvironment} from 'relay-runtime';
|
17
|
+
|
18
|
+
const invariant = require('invariant');
|
19
|
+
|
20
|
+
const TEMPORARY_RETAIN_DURATION_MS = 5 * 60 * 1000;
|
21
|
+
|
22
|
+
/**
|
23
|
+
* Allows you to retain a resource as part of a component lifecycle accounting
|
24
|
+
* for Suspense. You temporarily retain the resource during render, then
|
25
|
+
* permanently retain it during commit and release it during unmount.
|
26
|
+
*/
|
27
|
+
class SuspenseResource {
|
28
|
+
_retainCount = 0;
|
29
|
+
_retainDisposable: ?Disposable = null;
|
30
|
+
_releaseTemporaryRetain: ?() => void = null;
|
31
|
+
_retain: IEnvironment => Disposable;
|
32
|
+
|
33
|
+
constructor(retain: (environment: IEnvironment) => Disposable) {
|
34
|
+
this._retain = (environment: IEnvironment): Disposable => {
|
35
|
+
this._retainCount++;
|
36
|
+
if (this._retainCount === 1) {
|
37
|
+
this._retainDisposable = retain(environment);
|
38
|
+
}
|
39
|
+
return {
|
40
|
+
dispose: () => {
|
41
|
+
this._retainCount = Math.max(0, this._retainCount - 1);
|
42
|
+
if (this._retainCount === 0) {
|
43
|
+
invariant(
|
44
|
+
this._retainDisposable != null,
|
45
|
+
'Relay: Expected disposable to release query to be defined.' +
|
46
|
+
"If you're seeing this, this is likely a bug in Relay.",
|
47
|
+
);
|
48
|
+
this._retainDisposable.dispose();
|
49
|
+
this._retainDisposable = null;
|
50
|
+
}
|
51
|
+
},
|
52
|
+
};
|
53
|
+
};
|
54
|
+
}
|
55
|
+
|
56
|
+
temporaryRetain(environment: IEnvironment): Disposable {
|
57
|
+
// If we're executing in a server environment, there's no need
|
58
|
+
// to create temporary retains, since the component will never commit.
|
59
|
+
if (environment.isServer()) {
|
60
|
+
return {dispose: () => {}};
|
61
|
+
}
|
62
|
+
|
63
|
+
// temporaryRetain is called during the render phase. However,
|
64
|
+
// given that we can't tell if this render will eventually commit or not,
|
65
|
+
// we create a timer to autodispose of this retain in case the associated
|
66
|
+
// component never commits.
|
67
|
+
// If the component /does/ commit, permanentRetain will clear this timeout
|
68
|
+
// and permanently retain the data.
|
69
|
+
const retention = this._retain(environment);
|
70
|
+
let releaseQueryTimeout = null;
|
71
|
+
const releaseTemporaryRetain = () => {
|
72
|
+
clearTimeout(releaseQueryTimeout);
|
73
|
+
releaseQueryTimeout = null;
|
74
|
+
this._releaseTemporaryRetain = null;
|
75
|
+
retention.dispose();
|
76
|
+
};
|
77
|
+
releaseQueryTimeout = setTimeout(
|
78
|
+
releaseTemporaryRetain,
|
79
|
+
TEMPORARY_RETAIN_DURATION_MS,
|
80
|
+
);
|
81
|
+
|
82
|
+
// NOTE: Since temporaryRetain can be called multiple times, we release
|
83
|
+
// the previous temporary retain after we re-establish a new one, since
|
84
|
+
// we only ever need a single temporary retain until the permanent retain is
|
85
|
+
// established.
|
86
|
+
// temporaryRetain may be called multiple times by React during the render
|
87
|
+
// phase, as well as multiple times by other query components that are
|
88
|
+
// rendering the same query/variables.
|
89
|
+
this._releaseTemporaryRetain?.();
|
90
|
+
this._releaseTemporaryRetain = releaseTemporaryRetain;
|
91
|
+
|
92
|
+
return {
|
93
|
+
dispose: () => {
|
94
|
+
this._releaseTemporaryRetain?.();
|
95
|
+
},
|
96
|
+
};
|
97
|
+
}
|
98
|
+
|
99
|
+
permanentRetain(environment: IEnvironment): Disposable {
|
100
|
+
const disposable = this._retain(environment);
|
101
|
+
this.releaseTemporaryRetain();
|
102
|
+
return disposable;
|
103
|
+
}
|
104
|
+
|
105
|
+
releaseTemporaryRetain(): void {
|
106
|
+
this._releaseTemporaryRetain?.();
|
107
|
+
this._releaseTemporaryRetain = null;
|
108
|
+
}
|
109
|
+
|
110
|
+
getRetainCount(): number {
|
111
|
+
return this._retainCount;
|
112
|
+
}
|
113
|
+
}
|
114
|
+
|
115
|
+
module.exports = SuspenseResource;
|