react-relay 0.0.0-main-1e923be9 → 0.0.0-main-764af24d

Sign up to get free protection for your applications and to get access to all the features.
@@ -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;
@@ -0,0 +1,418 @@
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
+ CacheConfig,
18
+ FetchPolicy,
19
+ IEnvironment,
20
+ ReaderFragment,
21
+ ReaderSelector,
22
+ SelectorData,
23
+ Snapshot,
24
+ } from 'relay-runtime';
25
+ import type {MissingClientEdgeRequestInfo} from 'relay-runtime/store/RelayStoreTypes';
26
+
27
+ const useRelayEnvironment = require('../useRelayEnvironment');
28
+ const getQueryResultOrFetchQuery = require('./getQueryResultOrFetchQuery_REACT_CACHE');
29
+ const invariant = require('invariant');
30
+ const {useDebugValue, useEffect, useMemo, useRef, useState} = require('react');
31
+ const {
32
+ areEqualSelectors,
33
+ createOperationDescriptor,
34
+ getPendingOperationsForFragment,
35
+ getSelector,
36
+ getVariablesFromFragment,
37
+ recycleNodesInto,
38
+ reportMissingRequiredFields,
39
+ } = require('relay-runtime');
40
+
41
+ type FragmentQueryOptions = {|
42
+ fetchPolicy?: FetchPolicy,
43
+ networkCacheConfig?: CacheConfig,
44
+ |};
45
+
46
+ type FragmentState = $ReadOnly<
47
+ | {|kind: 'bailout'|}
48
+ | {|kind: 'singular', snapshot: Snapshot, epoch: number|}
49
+ | {|kind: 'plural', snapshots: $ReadOnlyArray<Snapshot>, epoch: number|},
50
+ >;
51
+
52
+ type StateUpdater<T> = (T | (T => T)) => void;
53
+
54
+ function isMissingData(state: FragmentState): boolean {
55
+ if (state.kind === 'bailout') {
56
+ return false;
57
+ } else if (state.kind === 'singular') {
58
+ return state.snapshot.isMissingData;
59
+ } else {
60
+ return state.snapshots.some(s => s.isMissingData);
61
+ }
62
+ }
63
+
64
+ function getMissingClientEdges(
65
+ state: FragmentState,
66
+ ): $ReadOnlyArray<MissingClientEdgeRequestInfo> | null {
67
+ if (state.kind === 'bailout') {
68
+ return null;
69
+ } else if (state.kind === 'singular') {
70
+ return state.snapshot.missingClientEdges ?? null;
71
+ } else {
72
+ let edges = null;
73
+ for (const snapshot of state.snapshots) {
74
+ if (snapshot.missingClientEdges) {
75
+ edges = edges ?? [];
76
+ for (const edge of snapshot.missingClientEdges) {
77
+ edges.push(edge);
78
+ }
79
+ }
80
+ }
81
+ return edges;
82
+ }
83
+ }
84
+
85
+ function reportMissingRequiredFieldsForState(
86
+ environment: IEnvironment,
87
+ state: FragmentState,
88
+ ): void {
89
+ if (state.kind === 'singular') {
90
+ if (state.snapshot.missingRequiredFields != null) {
91
+ reportMissingRequiredFields(
92
+ environment,
93
+ state.snapshot.missingRequiredFields,
94
+ );
95
+ }
96
+ } else if (state.kind === 'plural') {
97
+ for (const snapshot of state.snapshots) {
98
+ if (snapshot.missingRequiredFields != null) {
99
+ reportMissingRequiredFields(
100
+ environment,
101
+ snapshot.missingRequiredFields,
102
+ );
103
+ }
104
+ }
105
+ }
106
+ }
107
+
108
+ function handleMissedUpdates(
109
+ environment: IEnvironment,
110
+ state: FragmentState,
111
+ setState: StateUpdater<FragmentState>,
112
+ ): void {
113
+ if (state.kind === 'bailout') {
114
+ return;
115
+ }
116
+ const currentEpoch = environment.getStore().getEpoch();
117
+ if (currentEpoch === state.epoch) {
118
+ return;
119
+ }
120
+ // The store has updated since we rendered (without us being subscribed yet),
121
+ // so check for any updates to the data we're rendering:
122
+ if (state.kind === 'singular') {
123
+ const currentSnapshot = environment.lookup(state.snapshot.selector);
124
+ const updatedData = recycleNodesInto(
125
+ state.snapshot.data,
126
+ currentSnapshot.data,
127
+ );
128
+ if (updatedData !== state.snapshot.data) {
129
+ setState({
130
+ kind: 'singular',
131
+ snapshot: currentSnapshot,
132
+ epoch: currentEpoch,
133
+ });
134
+ }
135
+ } else {
136
+ let updates = null;
137
+ for (let index = 0; index < state.snapshots.length; index++) {
138
+ const snapshot = state.snapshots[index];
139
+ const currentSnapshot = environment.lookup(snapshot.selector);
140
+ const updatedData = recycleNodesInto(snapshot.data, currentSnapshot.data);
141
+ if (updatedData !== snapshot.data) {
142
+ updates =
143
+ updates === null ? new Array(state.snapshots.length) : updates;
144
+ updates[index] = snapshot;
145
+ }
146
+ }
147
+ if (updates !== null) {
148
+ const theUpdates = updates; // preserve flow refinement.
149
+ setState(existing => {
150
+ invariant(
151
+ existing.kind === 'plural',
152
+ 'Cannot go from singular to plural or from bailout to plural.',
153
+ );
154
+ const updated = [...existing.snapshots];
155
+ for (let index = 0; index < theUpdates.length; index++) {
156
+ const updatedSnapshot = theUpdates[index];
157
+ if (updatedSnapshot) {
158
+ updated[index] = updatedSnapshot;
159
+ }
160
+ }
161
+ return {kind: 'plural', snapshots: updated, epoch: currentEpoch};
162
+ });
163
+ }
164
+ }
165
+ }
166
+
167
+ function handleMissingClientEdge(
168
+ environment: IEnvironment,
169
+ parentFragmentNode: ReaderFragment,
170
+ parentFragmentRef: mixed,
171
+ missingClientEdgeRequestInfo: MissingClientEdgeRequestInfo,
172
+ queryOptions?: FragmentQueryOptions,
173
+ ): void {
174
+ const originalVariables = getVariablesFromFragment(
175
+ parentFragmentNode,
176
+ parentFragmentRef,
177
+ );
178
+ const variables = {
179
+ ...originalVariables,
180
+ id: missingClientEdgeRequestInfo.clientEdgeDestinationID, // TODO should be a reserved name
181
+ };
182
+ const queryOperationDescriptor = createOperationDescriptor(
183
+ missingClientEdgeRequestInfo.request,
184
+ variables,
185
+ queryOptions?.networkCacheConfig,
186
+ );
187
+ // This may suspend. We don't need to do anything with the results; all we're
188
+ // doing here is started the query if needed and retaining and releasing it
189
+ // according to the component mount/suspense cycle; getQueryResultOrFetchQuery
190
+ // already handles this by itself.
191
+ getQueryResultOrFetchQuery(
192
+ environment,
193
+ queryOperationDescriptor,
194
+ queryOptions?.fetchPolicy,
195
+ );
196
+ }
197
+
198
+ function subscribeToSnapshot(
199
+ environment: IEnvironment,
200
+ state: FragmentState,
201
+ setState: StateUpdater<FragmentState>,
202
+ ): () => void {
203
+ if (state.kind === 'bailout') {
204
+ return () => {};
205
+ } else if (state.kind === 'singular') {
206
+ const disposable = environment.subscribe(state.snapshot, latestSnapshot => {
207
+ setState({
208
+ kind: 'singular',
209
+ snapshot: latestSnapshot,
210
+ epoch: environment.getStore().getEpoch(),
211
+ });
212
+ });
213
+ return () => {
214
+ disposable.dispose();
215
+ };
216
+ } else {
217
+ const disposables = state.snapshots.map((snapshot, index) =>
218
+ environment.subscribe(snapshot, latestSnapshot => {
219
+ setState(existing => {
220
+ invariant(
221
+ existing.kind === 'plural',
222
+ 'Cannot go from singular to plural or from bailout to plural.',
223
+ );
224
+ const updated = [...existing.snapshots];
225
+ updated[index] = latestSnapshot;
226
+ return {
227
+ kind: 'plural',
228
+ snapshots: updated,
229
+ epoch: environment.getStore().getEpoch(),
230
+ };
231
+ });
232
+ }),
233
+ );
234
+ return () => {
235
+ for (const d of disposables) {
236
+ d.dispose();
237
+ }
238
+ };
239
+ }
240
+ }
241
+
242
+ function getFragmentState(
243
+ environment: IEnvironment,
244
+ fragmentSelector: ?ReaderSelector,
245
+ ): FragmentState {
246
+ if (fragmentSelector == null) {
247
+ return {kind: 'bailout'};
248
+ } else if (fragmentSelector.kind === 'PluralReaderSelector') {
249
+ return {
250
+ kind: 'plural',
251
+ snapshots: fragmentSelector.selectors.map(s => environment.lookup(s)),
252
+ epoch: environment.getStore().getEpoch(),
253
+ };
254
+ } else {
255
+ return {
256
+ kind: 'singular',
257
+ snapshot: environment.lookup(fragmentSelector),
258
+ epoch: environment.getStore().getEpoch(),
259
+ };
260
+ }
261
+ }
262
+
263
+ // fragmentNode cannot change during the lifetime of the component, though fragmentRef may change.
264
+ function useFragmentInternal_REACT_CACHE(
265
+ fragmentNode: ReaderFragment,
266
+ fragmentRef: mixed,
267
+ hookDisplayName: string,
268
+ queryOptions?: FragmentQueryOptions,
269
+ fragmentKey?: string,
270
+ ): {|
271
+ data: ?SelectorData | Array<?SelectorData>,
272
+ disableStoreUpdates: () => void,
273
+ enableStoreUpdates: () => void,
274
+ |} {
275
+ const fragmentSelector = getSelector(fragmentNode, fragmentRef);
276
+
277
+ if (fragmentNode?.metadata?.plural === true) {
278
+ invariant(
279
+ Array.isArray(fragmentRef),
280
+ 'Relay: Expected fragment pointer%s for fragment `%s` to be ' +
281
+ 'an array, instead got `%s`. Remove `@relay(plural: true)` ' +
282
+ 'from fragment `%s` to allow the prop to be an object.',
283
+ fragmentKey != null ? ` for key \`${fragmentKey}\`` : '',
284
+ fragmentNode.name,
285
+ typeof fragmentRef,
286
+ fragmentNode.name,
287
+ );
288
+ } else {
289
+ invariant(
290
+ !Array.isArray(fragmentRef),
291
+ 'Relay: Expected fragment pointer%s for fragment `%s` not to be ' +
292
+ 'an array, instead got `%s`. Add `@relay(plural: true)` ' +
293
+ 'to fragment `%s` to allow the prop to be an array.',
294
+ fragmentKey != null ? ` for key \`${fragmentKey}\`` : '',
295
+ fragmentNode.name,
296
+ typeof fragmentRef,
297
+ fragmentNode.name,
298
+ );
299
+ }
300
+ invariant(
301
+ fragmentRef == null || fragmentSelector != null,
302
+ 'Relay: Expected to receive an object where `...%s` was spread, ' +
303
+ 'but the fragment reference was not found`. This is most ' +
304
+ 'likely the result of:\n' +
305
+ "- Forgetting to spread `%s` in `%s`'s parent's fragment.\n" +
306
+ '- Conditionally fetching `%s` but unconditionally passing %s prop ' +
307
+ 'to `%s`. If the parent fragment only fetches the fragment conditionally ' +
308
+ '- with e.g. `@include`, `@skip`, or inside a `... on SomeType { }` ' +
309
+ 'spread - then the fragment reference will not exist. ' +
310
+ 'In this case, pass `null` if the conditions for evaluating the ' +
311
+ 'fragment are not met (e.g. if the `@include(if)` value is false.)',
312
+ fragmentNode.name,
313
+ fragmentNode.name,
314
+ hookDisplayName,
315
+ fragmentNode.name,
316
+ fragmentKey == null ? 'a fragment reference' : `the \`${fragmentKey}\``,
317
+ hookDisplayName,
318
+ );
319
+
320
+ const environment = useRelayEnvironment();
321
+ const [rawState, setState] = useState<FragmentState>(() =>
322
+ getFragmentState(environment, fragmentSelector),
323
+ );
324
+
325
+ const [previousFragmentSelector, setPreviousFragmentSelector] =
326
+ useState(fragmentSelector);
327
+ if (!areEqualSelectors(fragmentSelector, previousFragmentSelector)) {
328
+ setPreviousFragmentSelector(fragmentSelector);
329
+ setState(getFragmentState(environment, fragmentSelector));
330
+ }
331
+
332
+ let state;
333
+ if (fragmentRef == null) {
334
+ state = {kind: 'bailout'};
335
+ } else if (rawState.kind === 'plural' && rawState.snapshots.length === 0) {
336
+ state = {kind: 'bailout'};
337
+ } else {
338
+ state = rawState;
339
+ }
340
+
341
+ // Handle the queries for any missing client edges; this may suspend.
342
+ // FIXME handle client edges in parallel.
343
+ const missingClientEdges = getMissingClientEdges(state);
344
+ if (missingClientEdges?.length) {
345
+ for (const edge of missingClientEdges) {
346
+ handleMissingClientEdge(
347
+ environment,
348
+ fragmentNode,
349
+ fragmentRef,
350
+ edge,
351
+ queryOptions,
352
+ );
353
+ }
354
+ }
355
+
356
+ if (isMissingData(state)) {
357
+ // Suspend if an active operation bears on this fragment, either the
358
+ // fragment's owner or some other mutation etc. that could affect it:
359
+ invariant(fragmentSelector != null, 'refinement, see invariants above');
360
+ const fragmentOwner =
361
+ fragmentSelector.kind === 'PluralReaderSelector'
362
+ ? fragmentSelector.selectors[0].owner
363
+ : fragmentSelector.owner;
364
+ const pendingOperationsResult = getPendingOperationsForFragment(
365
+ environment,
366
+ fragmentNode,
367
+ fragmentOwner,
368
+ );
369
+ if (pendingOperationsResult) {
370
+ throw pendingOperationsResult.promise;
371
+ }
372
+ // Report required fields only if we're not suspending, since that means
373
+ // they're missing even though we are out of options for possibly fetching them:
374
+ reportMissingRequiredFieldsForState(environment, state);
375
+ }
376
+
377
+ // Subscriptions:
378
+ const isMountedRef = useRef(false);
379
+ const isListeningForUpdatesRef = useRef(true);
380
+ function enableStoreUpdates() {
381
+ isListeningForUpdatesRef.current = true;
382
+ handleMissedUpdates(environment, state, setState);
383
+ }
384
+ function disableStoreUpdates() {
385
+ isListeningForUpdatesRef.current = false;
386
+ }
387
+ useEffect(() => {
388
+ const wasAlreadySubscribed = isMountedRef.current;
389
+ isMountedRef.current = true;
390
+ if (!wasAlreadySubscribed) {
391
+ handleMissedUpdates(environment, state, setState);
392
+ }
393
+ return subscribeToSnapshot(environment, state, setState);
394
+ }, [environment, state]);
395
+
396
+ const data = useMemo(
397
+ () =>
398
+ state.kind === 'bailout'
399
+ ? {}
400
+ : state.kind === 'singular'
401
+ ? state.snapshot.data
402
+ : state.snapshots.map(s => s.data),
403
+ [state],
404
+ );
405
+
406
+ if (__DEV__) {
407
+ // eslint-disable-next-line react-hooks/rules-of-hooks
408
+ useDebugValue({fragment: fragmentNode.name, data});
409
+ }
410
+
411
+ return {
412
+ data,
413
+ disableStoreUpdates,
414
+ enableStoreUpdates,
415
+ };
416
+ }
417
+
418
+ module.exports = useFragmentInternal_REACT_CACHE;