relay-runtime 18.2.0 → 19.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 (81) hide show
  1. package/experimental.js +1 -1
  2. package/experimental.js.flow +8 -6
  3. package/index.js +1 -1
  4. package/index.js.flow +3 -0
  5. package/lib/experimental.js +5 -2
  6. package/lib/index.js +3 -0
  7. package/lib/multi-actor-environment/ActorSpecificEnvironment.js +1 -1
  8. package/lib/mutations/RelayRecordSourceProxy.js +2 -1
  9. package/lib/mutations/createUpdatableProxy.js +1 -1
  10. package/lib/mutations/validateMutation.js +2 -2
  11. package/lib/network/RelayObservable.js +1 -3
  12. package/lib/network/wrapNetworkWithLogObserver.js +2 -2
  13. package/lib/query/fetchQuery.js +1 -1
  14. package/lib/store/DataChecker.js +4 -5
  15. package/lib/store/OperationExecutor.js +11 -0
  16. package/lib/store/RelayModernEnvironment.js +13 -4
  17. package/lib/store/RelayModernFragmentSpecResolver.js +4 -4
  18. package/lib/store/RelayModernStore.js +43 -21
  19. package/lib/store/RelayPublishQueue.js +11 -15
  20. package/lib/store/RelayReader.js +131 -151
  21. package/lib/store/RelayReferenceMarker.js +3 -4
  22. package/lib/store/RelayResponseNormalizer.js +47 -26
  23. package/lib/store/RelayStoreSubscriptions.js +2 -2
  24. package/lib/store/RelayStoreUtils.js +8 -0
  25. package/lib/store/ResolverFragments.js +2 -2
  26. package/lib/store/createRelayLoggingContext.js +17 -0
  27. package/lib/store/generateTypenamePrefixedDataID.js +9 -0
  28. package/lib/store/live-resolvers/LiveResolverCache.js +2 -1
  29. package/lib/store/live-resolvers/resolverDataInjector.js +4 -4
  30. package/lib/store/observeFragmentExperimental.js +60 -13
  31. package/lib/store/observeQueryExperimental.js +21 -0
  32. package/lib/util/RelayFeatureFlags.js +6 -1
  33. package/lib/util/handlePotentialSnapshotErrors.js +11 -8
  34. package/multi-actor-environment/ActorSpecificEnvironment.js.flow +1 -0
  35. package/mutations/RelayRecordSourceProxy.js.flow +4 -0
  36. package/mutations/createUpdatableProxy.js.flow +1 -1
  37. package/mutations/validateMutation.js.flow +3 -3
  38. package/network/RelayNetworkTypes.js.flow +3 -0
  39. package/network/RelayObservable.js.flow +1 -5
  40. package/network/wrapNetworkWithLogObserver.js.flow +19 -1
  41. package/package.json +1 -1
  42. package/query/fetchQuery.js.flow +1 -1
  43. package/store/DataChecker.js.flow +5 -2
  44. package/store/OperationExecutor.js.flow +12 -1
  45. package/store/RelayExperimentalGraphResponseTransform.js.flow +4 -4
  46. package/store/RelayModernEnvironment.js.flow +22 -6
  47. package/store/RelayModernFragmentSpecResolver.js.flow +6 -6
  48. package/store/RelayModernSelector.js.flow +2 -0
  49. package/store/RelayModernStore.js.flow +74 -27
  50. package/store/RelayPublishQueue.js.flow +32 -21
  51. package/store/RelayReader.js.flow +159 -96
  52. package/store/RelayReferenceMarker.js.flow +3 -4
  53. package/store/RelayResponseNormalizer.js.flow +93 -67
  54. package/store/RelayStoreSubscriptions.js.flow +2 -2
  55. package/store/RelayStoreTypes.js.flow +33 -4
  56. package/store/RelayStoreUtils.js.flow +29 -0
  57. package/store/ResolverCache.js.flow +2 -2
  58. package/store/ResolverFragments.js.flow +5 -3
  59. package/store/StoreInspector.js.flow +5 -0
  60. package/store/createRelayContext.js.flow +3 -2
  61. package/store/createRelayLoggingContext.js.flow +46 -0
  62. package/store/generateTypenamePrefixedDataID.js.flow +25 -0
  63. package/store/live-resolvers/LiveResolverCache.js.flow +2 -1
  64. package/store/live-resolvers/resolverDataInjector.js.flow +10 -6
  65. package/store/observeFragmentExperimental.js.flow +82 -28
  66. package/store/observeQueryExperimental.js.flow +61 -0
  67. package/store/waitForFragmentExperimental.js.flow +4 -3
  68. package/util/NormalizationNode.js.flow +2 -1
  69. package/util/RelayConcreteNode.js.flow +2 -0
  70. package/util/RelayError.js.flow +1 -0
  71. package/util/RelayFeatureFlags.js.flow +18 -0
  72. package/util/RelayRuntimeTypes.js.flow +6 -3
  73. package/util/getPaginationVariables.js.flow +2 -0
  74. package/util/handlePotentialSnapshotErrors.js.flow +23 -11
  75. package/util/registerEnvironmentWithDevTools.js.flow +4 -2
  76. package/util/withProvidedVariables.js.flow +1 -0
  77. package/util/withStartAndDuration.js.flow +3 -0
  78. package/relay-runtime-experimental.js +0 -4
  79. package/relay-runtime-experimental.min.js +0 -9
  80. package/relay-runtime.js +0 -4
  81. package/relay-runtime.min.js +0 -9
@@ -12,12 +12,17 @@
12
12
  'use strict';
13
13
  import type ActorSpecificEnvironment from '../multi-actor-environment/ActorSpecificEnvironment';
14
14
  import type RelayModernEnvironment from '../store/RelayModernEnvironment';
15
+ import type {
16
+ LogRequestInfoFunction,
17
+ OperationAvailability,
18
+ } from '../store/RelayStoreTypes';
15
19
  import type {RequestParameters} from '../util/RelayConcreteNode';
16
20
  import type {CacheConfig, Variables} from '../util/RelayRuntimeTypes';
17
21
  import type {
18
22
  GraphQLResponse,
19
23
  INetwork,
20
24
  UploadableMap,
25
+ preprocessResponseFunction,
21
26
  } from './RelayNetworkTypes';
22
27
  import type RelayObservable from './RelayObservable';
23
28
  import type {Subscription} from './RelayObservable';
@@ -42,6 +47,10 @@ function wrapNetworkWithLogObserver(
42
47
  variables: Variables,
43
48
  cacheConfig: CacheConfig,
44
49
  uploadables?: ?UploadableMap,
50
+ _?: ?LogRequestInfoFunction,
51
+ encryptedVariables?: ?string,
52
+ preprocessResponse?: ?preprocessResponseFunction,
53
+ checkOperation?: () => OperationAvailability,
45
54
  ): RelayObservable<GraphQLResponse> {
46
55
  const networkRequestId = generateID();
47
56
  const logObserver = {
@@ -89,7 +98,16 @@ function wrapNetworkWithLogObserver(
89
98
  });
90
99
  };
91
100
  return network
92
- .execute(params, variables, cacheConfig, uploadables, logRequestInfo)
101
+ .execute(
102
+ params,
103
+ variables,
104
+ cacheConfig,
105
+ uploadables,
106
+ logRequestInfo,
107
+ encryptedVariables,
108
+ preprocessResponse,
109
+ checkOperation,
110
+ )
93
111
  .do(logObserver);
94
112
  },
95
113
  };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "relay-runtime",
3
3
  "description": "A core runtime for building GraphQL-driven applications.",
4
- "version": "18.2.0",
4
+ "version": "19.0.0",
5
5
  "keywords": [
6
6
  "graphql",
7
7
  "relay"
@@ -138,7 +138,7 @@ function fetchQuery<TVariables: Variables, TData, TRawResponse>(
138
138
  const fetchPolicy = options?.fetchPolicy ?? 'network-only';
139
139
 
140
140
  function readData(snapshot: Snapshot): TData {
141
- handlePotentialSnapshotErrors(environment, snapshot.errorResponseFields);
141
+ handlePotentialSnapshotErrors(environment, snapshot.fieldErrors);
142
142
  /* $FlowFixMe[incompatible-return] we assume readData returns the right
143
143
  * data just having written it from network or checked availability. */
144
144
  return snapshot.data;
@@ -90,6 +90,7 @@ function check(
90
90
  operationLoader,
91
91
  getDataID,
92
92
  shouldProcessClientComponents,
93
+ log,
93
94
  );
94
95
  const result = checker.check(node, dataID);
95
96
  if (log != null) {
@@ -123,6 +124,7 @@ class DataChecker {
123
124
  ActorIdentifier,
124
125
  [RelayRecordSourceMutator, RelayRecordSourceProxy],
125
126
  >;
127
+ _log: ?LogFunction;
126
128
 
127
129
  constructor(
128
130
  getSourceForActor: (actorIdentifier: ActorIdentifier) => RecordSource,
@@ -135,6 +137,7 @@ class DataChecker {
135
137
  operationLoader: ?OperationLoader,
136
138
  getDataID: GetDataID,
137
139
  shouldProcessClientComponents: ?boolean,
140
+ log: ?LogFunction,
138
141
  ) {
139
142
  this._getSourceForActor = getSourceForActor;
140
143
  this._getTargetForActor = getTargetForActor;
@@ -152,6 +155,7 @@ class DataChecker {
152
155
  this._recordWasMissing = false;
153
156
  this._variables = variables;
154
157
  this._shouldProcessClientComponents = shouldProcessClientComponents;
158
+ this._log = log;
155
159
  }
156
160
 
157
161
  _getMutatorAndRecordProxyForActor(
@@ -170,6 +174,7 @@ class DataChecker {
170
174
  this._getDataID,
171
175
  undefined,
172
176
  this._handlers,
177
+ this._log,
173
178
  );
174
179
  tuple = [mutator, recordSourceProxy];
175
180
  this._mutatorRecordSourceProxyCache.set(actorIdentifier, tuple);
@@ -449,8 +454,6 @@ class DataChecker {
449
454
  this._traverseSelections(selection.fragment.selections, dataID);
450
455
  break;
451
456
  case 'RelayResolver':
452
- this._checkResolver(selection, dataID);
453
- break;
454
457
  case 'RelayLiveResolver':
455
458
  this._checkResolver(selection, dataID);
456
459
  break;
@@ -245,6 +245,12 @@ class Executor<TMutation: MutationParameters> {
245
245
  cacheConfig: this._operation.request.cacheConfig ?? {},
246
246
  });
247
247
  },
248
+ unsubscribe: () => {
249
+ this._log({
250
+ name: 'execute.unsubscribe',
251
+ executeId: this._executeId,
252
+ });
253
+ },
248
254
  });
249
255
 
250
256
  if (
@@ -293,7 +299,7 @@ class Executor<TMutation: MutationParameters> {
293
299
  }
294
300
 
295
301
  _updateActiveState(): void {
296
- let activeState;
302
+ let activeState: ActiveState;
297
303
  switch (this._state) {
298
304
  case 'started': {
299
305
  activeState = 'active';
@@ -606,6 +612,7 @@ class Executor<TMutation: MutationParameters> {
606
612
  {
607
613
  actorIdentifier: this._actorIdentifier,
608
614
  getDataID: this._getDataID,
615
+ log: this._log,
609
616
  path: [],
610
617
  shouldProcessClientComponents: this._shouldProcessClientComponents,
611
618
  treatMissingFieldsAsNull,
@@ -715,6 +722,7 @@ class Executor<TMutation: MutationParameters> {
715
722
  {
716
723
  actorIdentifier: this._actorIdentifier,
717
724
  getDataID: this._getDataID,
725
+ log: this._log,
718
726
  path: followupPayload.path,
719
727
  treatMissingFieldsAsNull: this._treatMissingFieldsAsNull,
720
728
  shouldProcessClientComponents: this._shouldProcessClientComponents,
@@ -797,6 +805,7 @@ class Executor<TMutation: MutationParameters> {
797
805
  {
798
806
  actorIdentifier: this._actorIdentifier,
799
807
  getDataID: this._getDataID,
808
+ log: this._log,
800
809
  path: [],
801
810
  treatMissingFieldsAsNull: this._treatMissingFieldsAsNull,
802
811
  shouldProcessClientComponents: this._shouldProcessClientComponents,
@@ -1253,6 +1262,7 @@ class Executor<TMutation: MutationParameters> {
1253
1262
  {
1254
1263
  actorIdentifier: this._actorIdentifier,
1255
1264
  getDataID: this._getDataID,
1265
+ log: this._log,
1256
1266
  path: placeholder.path,
1257
1267
  treatMissingFieldsAsNull: this._treatMissingFieldsAsNull,
1258
1268
  shouldProcessClientComponents: this._shouldProcessClientComponents,
@@ -1480,6 +1490,7 @@ class Executor<TMutation: MutationParameters> {
1480
1490
  const relayPayload = this._normalizeResponse(response, selector, typeName, {
1481
1491
  actorIdentifier: this._actorIdentifier,
1482
1492
  getDataID: this._getDataID,
1493
+ log: this._log,
1483
1494
  path: [...normalizationPath, responseKey, String(itemIndex)],
1484
1495
  treatMissingFieldsAsNull: this._treatMissingFieldsAsNull,
1485
1496
  shouldProcessClientComponents: this._shouldProcessClientComponents,
@@ -222,10 +222,10 @@ export class GraphModeNormalizer {
222
222
  $streamID,
223
223
  __id: dataID,
224
224
  __typename: ROOT_TYPE,
225
- };
225
+ } as RecordChunk;
226
226
  yield {
227
227
  $kind: 'Complete',
228
- };
228
+ } as CompleteChunk;
229
229
  }
230
230
 
231
231
  *_flushFields(
@@ -247,9 +247,9 @@ export class GraphModeNormalizer {
247
247
  __typename: typename,
248
248
  __id: cacheKey,
249
249
  $streamID,
250
- };
250
+ } as RecordChunk;
251
251
  } else if (Object.keys(fields).length > 0) {
252
- yield {...fields, $kind: 'Extend', $streamID};
252
+ yield {...fields, $kind: 'Extend', $streamID} as ExtendChunk;
253
253
  }
254
254
  return $streamID;
255
255
  }
@@ -58,6 +58,7 @@ const defaultGetDataID = require('./defaultGetDataID');
58
58
  const defaultRelayFieldLogger = require('./defaultRelayFieldLogger');
59
59
  const normalizeResponse = require('./normalizeResponse');
60
60
  const OperationExecutor = require('./OperationExecutor');
61
+ const RelayModernStore = require('./RelayModernStore');
61
62
  const RelayPublishQueue = require('./RelayPublishQueue');
62
63
  const RelayRecordSource = require('./RelayRecordSource');
63
64
  const invariant = require('invariant');
@@ -71,7 +72,7 @@ export type EnvironmentConfig = {
71
72
  +network: INetwork,
72
73
  +normalizeResponse?: ?NormalizeResponseFunction,
73
74
  +scheduler?: ?TaskScheduler,
74
- +store: Store,
75
+ +store?: Store,
75
76
  +missingFieldHandlers?: ?$ReadOnlyArray<MissingFieldHandler>,
76
77
  +operationTracker?: ?OperationTracker,
77
78
  +getDataID?: ?GetDataID,
@@ -118,6 +119,15 @@ class RelayModernEnvironment implements IEnvironment {
118
119
  );
119
120
  }
120
121
  }
122
+ const store =
123
+ config.store ??
124
+ new RelayModernStore(new RelayRecordSource(), {
125
+ log: config.log,
126
+ operationLoader: config.operationLoader,
127
+ getDataID: config.getDataID,
128
+ shouldProcessClientComponents: config.shouldProcessClientComponents,
129
+ });
130
+
121
131
  this.__log = config.log ?? emptyFunction;
122
132
  this.relayFieldLogger = config.relayFieldLogger ?? defaultRelayFieldLogger;
123
133
  this._defaultRenderPolicy =
@@ -128,13 +138,14 @@ class RelayModernEnvironment implements IEnvironment {
128
138
  this._getDataID = config.getDataID ?? defaultGetDataID;
129
139
  this._missingFieldHandlers = config.missingFieldHandlers ?? [];
130
140
  this._publishQueue = new RelayPublishQueue(
131
- config.store,
141
+ store,
132
142
  config.handlerProvider ?? RelayDefaultHandlerProvider,
133
143
  this._getDataID,
134
144
  this._missingFieldHandlers,
145
+ this.__log,
135
146
  );
136
147
  this._scheduler = config.scheduler ?? null;
137
- this._store = config.store;
148
+ this._store = store;
138
149
  this.options = config.options;
139
150
  this._isServer = config.isServer ?? false;
140
151
  this._normalizeResponse = config.normalizeResponse ?? normalizeResponse;
@@ -327,13 +338,18 @@ class RelayModernEnvironment implements IEnvironment {
327
338
  operation: OperationDescriptor,
328
339
  }): RelayObservable<GraphQLResponse> {
329
340
  return this._execute({
330
- createSource: () =>
331
- this.getNetwork().execute(
341
+ createSource: () => {
342
+ return this.getNetwork().execute(
332
343
  operation.request.node.params,
333
344
  operation.request.variables,
334
345
  operation.request.cacheConfig || {},
335
346
  null,
336
- ),
347
+ undefined,
348
+ undefined,
349
+ undefined,
350
+ () => this.check(operation),
351
+ );
352
+ },
337
353
  isClientPayload: false,
338
354
  operation,
339
355
  optimisticConfig: null,
@@ -14,7 +14,7 @@
14
14
  import type {ConcreteRequest} from '../util/RelayConcreteNode';
15
15
  import type {Disposable, Variables} from '../util/RelayRuntimeTypes';
16
16
  import type {
17
- ErrorResponseFields,
17
+ FieldErrors,
18
18
  FragmentMap,
19
19
  FragmentSpecResolver,
20
20
  FragmentSpecResults,
@@ -228,7 +228,7 @@ class SelectorResolver {
228
228
  _data: ?SelectorData;
229
229
  _environment: IEnvironment;
230
230
  _isMissingData: boolean;
231
- _errorResponseFields: ?ErrorResponseFields;
231
+ _fieldErrors: ?FieldErrors;
232
232
  _rootIsQueryRenderer: boolean;
233
233
  _selector: SingularReaderSelector;
234
234
  _subscription: ?Disposable;
@@ -244,7 +244,7 @@ class SelectorResolver {
244
244
  this._callback = callback;
245
245
  this._data = snapshot.data;
246
246
  this._isMissingData = snapshot.isMissingData;
247
- this._errorResponseFields = snapshot.errorResponseFields;
247
+ this._fieldErrors = snapshot.fieldErrors;
248
248
  this._environment = environment;
249
249
  this._rootIsQueryRenderer = rootIsQueryRenderer;
250
250
  this._selector = selector;
@@ -325,7 +325,7 @@ class SelectorResolver {
325
325
  }
326
326
  }
327
327
  }
328
- handlePotentialSnapshotErrors(this._environment, this._errorResponseFields);
328
+ handlePotentialSnapshotErrors(this._environment, this._fieldErrors);
329
329
  return this._data;
330
330
  }
331
331
 
@@ -340,7 +340,7 @@ class SelectorResolver {
340
340
  const snapshot = this._environment.lookup(selector);
341
341
  this._data = recycleNodesInto(this._data, snapshot.data);
342
342
  this._isMissingData = snapshot.isMissingData;
343
- this._errorResponseFields = snapshot.errorResponseFields;
343
+ this._fieldErrors = snapshot.fieldErrors;
344
344
  this._selector = selector;
345
345
  this._subscription = this._environment.subscribe(snapshot, this._onChange);
346
346
  }
@@ -376,7 +376,7 @@ class SelectorResolver {
376
376
  _onChange = (snapshot: Snapshot): void => {
377
377
  this._data = snapshot.data;
378
378
  this._isMissingData = snapshot.isMissingData;
379
- this._errorResponseFields = snapshot.errorResponseFields;
379
+ this._fieldErrors = snapshot.fieldErrors;
380
380
  this._callback();
381
381
  };
382
382
  }
@@ -340,6 +340,7 @@ function getVariablesFromObject(
340
340
  const fragment = fragments[key];
341
341
  const item = object[key];
342
342
  const itemVariables = getVariablesFromFragment(fragment, item);
343
+ // $FlowFixMe[unsafe-object-assign]
343
344
  Object.assign(variables, itemVariables);
344
345
  }
345
346
  }
@@ -397,6 +398,7 @@ function getVariablesFromPluralFragment(
397
398
  if (value != null) {
398
399
  const itemVariables = getVariablesFromSingularFragment(fragment, value);
399
400
  if (itemVariables != null) {
401
+ // $FlowFixMe[unsafe-object-assign]
400
402
  Object.assign(variables, itemVariables);
401
403
  }
402
404
  }
@@ -107,6 +107,7 @@ class RelayModernStore implements Store {
107
107
  fetchTime: ?number,
108
108
  },
109
109
  >;
110
+ _shouldRetainWithinTTL_EXPERIMENTAL: boolean;
110
111
  _shouldScheduleGC: boolean;
111
112
  _storeSubscriptions: StoreSubscriptions;
112
113
  _updatedRecordIDs: DataIDSet;
@@ -127,6 +128,9 @@ class RelayModernStore implements Store {
127
128
  shouldProcessClientComponents?: ?boolean,
128
129
  resolverContext?: ResolverContext,
129
130
 
131
+ // Experimental
132
+ shouldRetainWithinTTL_EXPERIMENTAL?: boolean,
133
+
130
134
  // These additional config options are only used if the experimental
131
135
  // @outputType resolver feature is used
132
136
  treatMissingFieldsAsNull?: ?boolean,
@@ -147,6 +151,8 @@ class RelayModernStore implements Store {
147
151
  this._gcHoldCounter = 0;
148
152
  this._gcReleaseBufferSize =
149
153
  options?.gcReleaseBufferSize ?? DEFAULT_RELEASE_BUFFER_SIZE;
154
+ this._shouldRetainWithinTTL_EXPERIMENTAL =
155
+ options?.shouldRetainWithinTTL_EXPERIMENTAL ?? false;
150
156
  this._gcRun = null;
151
157
  this._gcScheduler = options?.gcScheduler ?? resolveImmediate;
152
158
  this._getDataID = options?.getDataID ?? defaultGetDataID;
@@ -165,14 +171,15 @@ class RelayModernStore implements Store {
165
171
  () => this._getMutableRecordSource(),
166
172
  this,
167
173
  );
174
+ this._resolverContext = options?.resolverContext;
168
175
  this._storeSubscriptions = new RelayStoreSubscriptions(
169
176
  options?.log,
170
177
  this._resolverCache,
178
+ this._resolverContext,
171
179
  );
172
180
  this._updatedRecordIDs = new Set();
173
181
  this._shouldProcessClientComponents =
174
182
  options?.shouldProcessClientComponents ?? false;
175
- this._resolverContext = options?.resolverContext;
176
183
 
177
184
  this._treatMissingFieldsAsNull = options?.treatMissingFieldsAsNull ?? false;
178
185
  this._actorIdentifier = options?.actorIdentifier;
@@ -315,7 +322,9 @@ class RelayModernStore implements Store {
315
322
  rootEntry.fetchTime <= Date.now() - _queryCacheExpirationTime;
316
323
 
317
324
  if (rootEntryIsStale) {
318
- this._roots.delete(id);
325
+ if (!this._shouldRetainWithinTTL_EXPERIMENTAL) {
326
+ this._roots.delete(id);
327
+ }
319
328
  this.scheduleGC();
320
329
  } else {
321
330
  this._releaseBuffer.push(id);
@@ -325,8 +334,10 @@ class RelayModernStore implements Store {
325
334
  // buffer have a refCount of 0.
326
335
  if (this._releaseBuffer.length > this._gcReleaseBufferSize) {
327
336
  const _id = this._releaseBuffer.shift();
328
- // $FlowFixMe[incompatible-call]
329
- this._roots.delete(_id);
337
+ if (!this._shouldRetainWithinTTL_EXPERIMENTAL) {
338
+ // $FlowFixMe[incompatible-call]
339
+ this._roots.delete(_id);
340
+ }
330
341
  this.scheduleGC();
331
342
  }
332
343
  }
@@ -688,6 +699,14 @@ class RelayModernStore implements Store {
688
699
  };
689
700
 
690
701
  *_collect(): Generator<void, void, void> {
702
+ if (
703
+ this._shouldRetainWithinTTL_EXPERIMENTAL &&
704
+ this._queryCacheExpirationTime == null
705
+ ) {
706
+ // Null expiration time indicates infinite TTL, so we don't need to
707
+ // run GC.
708
+ return;
709
+ }
691
710
  /* eslint-disable no-labels */
692
711
  const log = this.__log;
693
712
  top: while (true) {
@@ -699,8 +718,30 @@ class RelayModernStore implements Store {
699
718
  const startEpoch = this._currentWriteEpoch;
700
719
  const references = new Set<DataID>();
701
720
 
702
- // Mark all records that are traversable from a root
703
- for (const {operation} of this._roots.values()) {
721
+ for (const [
722
+ dataID,
723
+ {operation, refCount, fetchTime},
724
+ ] of this._roots.entries()) {
725
+ if (this._shouldRetainWithinTTL_EXPERIMENTAL) {
726
+ // Do not mark records that should be garbage collected
727
+ const {_queryCacheExpirationTime} = this;
728
+ invariant(
729
+ _queryCacheExpirationTime != null,
730
+ 'Query cache expiration time should be non-null if executing GC',
731
+ );
732
+ const recordHasExpired =
733
+ fetchTime == null ||
734
+ fetchTime <= Date.now() - _queryCacheExpirationTime;
735
+ const recordShouldBeCollected =
736
+ recordHasExpired &&
737
+ refCount === 0 &&
738
+ !this._releaseBuffer.includes(dataID);
739
+ if (recordShouldBeCollected) {
740
+ continue;
741
+ }
742
+ }
743
+
744
+ // Mark all records that are traversable from a root that is still valid
704
745
  const selector = operation.root;
705
746
  RelayReferenceMarker.mark(
706
747
  this._recordSource,
@@ -723,31 +764,36 @@ class RelayModernStore implements Store {
723
764
  }
724
765
  }
725
766
 
726
- // Sweep records without references
727
- if (references.size === 0) {
728
- // Short-circuit if *nothing* is referenced
729
- this._recordSource.clear();
730
- } else {
731
- // Evict any unreferenced nodes
732
- const storeIDs = this._recordSource.getRecordIDs();
733
- for (let ii = 0; ii < storeIDs.length; ii++) {
734
- const dataID = storeIDs[ii];
735
- if (!references.has(dataID)) {
736
- const record = this._recordSource.get(dataID);
737
- if (record != null) {
738
- const maybeResolverSubscription = RelayModernRecord.getValue(
739
- record,
740
- RELAY_RESOLVER_LIVE_STATE_SUBSCRIPTION_KEY,
741
- );
742
- if (maybeResolverSubscription != null) {
743
- // $FlowFixMe - this value if it is not null, it is a function
744
- maybeResolverSubscription();
745
- }
767
+ // NOTE: It may be tempting to use `this._recordSource.clear()`
768
+ // when no references are found, but that would prevent calling
769
+ // maybeResolverSubscription() on any records that have an active
770
+ // resolver subscription. This would result in a memory leak.
771
+
772
+ // Evict any unreferenced nodes
773
+ const storeIDs = this._recordSource.getRecordIDs();
774
+ for (let ii = 0; ii < storeIDs.length; ii++) {
775
+ const dataID = storeIDs[ii];
776
+ if (!references.has(dataID)) {
777
+ const record = this._recordSource.get(dataID);
778
+ if (record != null) {
779
+ const maybeResolverSubscription = RelayModernRecord.getValue(
780
+ record,
781
+ RELAY_RESOLVER_LIVE_STATE_SUBSCRIPTION_KEY,
782
+ );
783
+ if (maybeResolverSubscription != null) {
784
+ // $FlowFixMe - this value if it is not null, it is a function
785
+ maybeResolverSubscription();
746
786
  }
747
- this._recordSource.remove(dataID);
787
+ }
788
+ this._recordSource.remove(dataID);
789
+ if (this._shouldRetainWithinTTL_EXPERIMENTAL) {
790
+ // Note: A record that was never retained will not be in the roots map
791
+ // but the following line should not throw
792
+ this._roots.delete(dataID);
748
793
  }
749
794
  }
750
795
  }
796
+
751
797
  if (log != null) {
752
798
  log({
753
799
  name: 'store.gc.end',
@@ -765,6 +811,7 @@ class RelayModernStore implements Store {
765
811
  return {
766
812
  path,
767
813
  getDataID: this._getDataID,
814
+ log: this.__log,
768
815
  treatMissingFieldsAsNull: this._treatMissingFieldsAsNull,
769
816
  shouldProcessClientComponents: this._shouldProcessClientComponents,
770
817
  actorIdentifier: this._actorIdentifier,
@@ -15,6 +15,7 @@ import type {HandlerProvider} from '../handlers/RelayDefaultHandlerProvider';
15
15
  import type {Disposable} from '../util/RelayRuntimeTypes';
16
16
  import type {GetDataID} from './RelayResponseNormalizer';
17
17
  import type {
18
+ LogFunction,
18
19
  MissingFieldHandler,
19
20
  MutationParameters,
20
21
  OperationDescriptor,
@@ -33,6 +34,7 @@ import type {
33
34
  const RelayRecordSourceMutator = require('../mutations/RelayRecordSourceMutator');
34
35
  const RelayRecordSourceProxy = require('../mutations/RelayRecordSourceProxy');
35
36
  const RelayRecordSourceSelectorProxy = require('../mutations/RelayRecordSourceSelectorProxy');
37
+ const RelayFeatureFlags = require('../util/RelayFeatureFlags');
36
38
  const RelayReader = require('./RelayReader');
37
39
  const RelayRecordSource = require('./RelayRecordSource');
38
40
  const invariant = require('invariant');
@@ -60,8 +62,10 @@ type PendingUpdater = {
60
62
  const _global: typeof global | $FlowFixMe =
61
63
  typeof global !== 'undefined'
62
64
  ? global
63
- : typeof window !== 'undefined'
64
- ? window
65
+ : // $FlowFixMe[cannot-resolve-name]
66
+ typeof window !== 'undefined'
67
+ ? // $FlowFixMe[cannot-resolve-name]
68
+ window
65
69
  : undefined;
66
70
 
67
71
  const applyWithGuard =
@@ -84,6 +88,7 @@ class RelayPublishQueue implements PublishQueue {
84
88
  _handlerProvider: ?HandlerProvider;
85
89
  _missingFieldHandlers: $ReadOnlyArray<MissingFieldHandler>;
86
90
  _getDataID: GetDataID;
91
+ _log: ?LogFunction;
87
92
 
88
93
  _hasStoreSnapshot: boolean;
89
94
  // True if the next `run()` should apply the backup and rerun all optimistic
@@ -112,6 +117,7 @@ class RelayPublishQueue implements PublishQueue {
112
117
  handlerProvider?: ?HandlerProvider,
113
118
  getDataID: GetDataID,
114
119
  missingFieldHandlers: $ReadOnlyArray<MissingFieldHandler>,
120
+ log: LogFunction,
115
121
  ) {
116
122
  this._hasStoreSnapshot = false;
117
123
  this._handlerProvider = handlerProvider || null;
@@ -123,6 +129,7 @@ class RelayPublishQueue implements PublishQueue {
123
129
  this._gcHold = null;
124
130
  this._getDataID = getDataID;
125
131
  this._missingFieldHandlers = missingFieldHandlers;
132
+ this._log = log;
126
133
  }
127
134
 
128
135
  /**
@@ -219,24 +226,27 @@ class RelayPublishQueue implements PublishQueue {
219
226
  this._pendingOptimisticUpdates.size === 0 &&
220
227
  !runWillClearGcHold;
221
228
 
222
- if (__DEV__) {
223
- warning(
224
- !runIsANoop,
225
- 'RelayPublishQueue.run was called, but the call would have been a noop.',
226
- );
227
- warning(
228
- this._isRunning !== true,
229
- 'A store update was detected within another store update. Please ' +
230
- "make sure new store updates aren't being executed within an " +
231
- 'updater function for a different update.',
232
- );
233
- this._isRunning = true;
234
- }
229
+ warning(
230
+ !runIsANoop,
231
+ 'RelayPublishQueue.run was called, but the call would have been a noop.',
232
+ );
233
+ RelayFeatureFlags.DISALLOW_NESTED_UPDATES
234
+ ? invariant(
235
+ this._isRunning !== true,
236
+ 'A store update was detected within another store update. Please ' +
237
+ "make sure new store updates aren't being executed within an " +
238
+ 'updater function for a different update.',
239
+ )
240
+ : warning(
241
+ this._isRunning !== true,
242
+ 'A store update was detected within another store update. Please ' +
243
+ "make sure new store updates aren't being executed within an " +
244
+ 'updater function for a different update.',
245
+ );
246
+ this._isRunning = true;
235
247
 
236
248
  if (runIsANoop) {
237
- if (__DEV__) {
238
- this._isRunning = false;
239
- }
249
+ this._isRunning = false;
240
250
  return [];
241
251
  }
242
252
 
@@ -268,9 +278,7 @@ class RelayPublishQueue implements PublishQueue {
268
278
  this._gcHold = null;
269
279
  }
270
280
  }
271
- if (__DEV__) {
272
- this._isRunning = false;
273
- }
281
+ this._isRunning = false;
274
282
  return this._store.notify(sourceOperation, invalidatedStore);
275
283
  }
276
284
 
@@ -292,6 +300,7 @@ class RelayPublishQueue implements PublishQueue {
292
300
  this._getDataID,
293
301
  this._handlerProvider,
294
302
  this._missingFieldHandlers,
303
+ this._log,
295
304
  );
296
305
  if (fieldPayloads && fieldPayloads.length) {
297
306
  fieldPayloads.forEach(fieldPayload => {
@@ -355,6 +364,7 @@ class RelayPublishQueue implements PublishQueue {
355
364
  this._getDataID,
356
365
  this._handlerProvider,
357
366
  this._missingFieldHandlers,
367
+ this._log,
358
368
  );
359
369
  applyWithGuard(
360
370
  updater,
@@ -388,6 +398,7 @@ class RelayPublishQueue implements PublishQueue {
388
398
  this._getDataID,
389
399
  this._handlerProvider,
390
400
  this._missingFieldHandlers,
401
+ this._log,
391
402
  );
392
403
 
393
404
  // $FlowFixMe[unclear-type] see explanation above.