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
@@ -29,7 +29,9 @@ import type {RelayErrorTrie} from './RelayErrorTrie';
29
29
  import type {
30
30
  FollowupPayload,
31
31
  HandleFieldPayload,
32
+ IdCollisionTypenameLogEvent,
32
33
  IncrementalDataPlaceholder,
34
+ LogFunction,
33
35
  MutableRecordSource,
34
36
  NormalizationSelector,
35
37
  Record,
@@ -72,6 +74,7 @@ export type GetDataID = (
72
74
  export type NormalizationOptions = {
73
75
  +getDataID: GetDataID,
74
76
  +treatMissingFieldsAsNull: boolean,
77
+ +log: ?LogFunction,
75
78
  +path?: $ReadOnlyArray<string>,
76
79
  +shouldProcessClientComponents?: ?boolean,
77
80
  +actorIdentifier?: ?ActorIdentifier,
@@ -116,6 +119,7 @@ class RelayResponseNormalizer {
116
119
  _variables: Variables;
117
120
  _shouldProcessClientComponents: ?boolean;
118
121
  _errorTrie: RelayErrorTrie | null;
122
+ _log: ?LogFunction;
119
123
 
120
124
  constructor(
121
125
  recordSource: MutableRecordSource,
@@ -134,6 +138,7 @@ class RelayResponseNormalizer {
134
138
  this._recordSource = recordSource;
135
139
  this._variables = variables;
136
140
  this._shouldProcessClientComponents = options.shouldProcessClientComponents;
141
+ this._log = options.log;
137
142
  }
138
143
 
139
144
  normalizeResponse(
@@ -319,8 +324,6 @@ class RelayResponseNormalizer {
319
324
  this._normalizeActorChange(selection, record, data);
320
325
  break;
321
326
  case 'RelayResolver':
322
- this._normalizeResolver(selection, record, data);
323
- break;
324
327
  case 'RelayLiveResolver':
325
328
  this._normalizeResolver(selection, record, data);
326
329
  break;
@@ -472,12 +475,11 @@ class RelayResponseNormalizer {
472
475
  const responseKey = selection.alias || selection.name;
473
476
  const storageKey = getStorageKey(selection, this._variables);
474
477
  const fieldValue = data[responseKey];
475
- if (
476
- fieldValue == null ||
477
- (RelayFeatureFlags.ENABLE_NONCOMPLIANT_ERROR_HANDLING_ON_LISTS &&
478
- Array.isArray(fieldValue) &&
479
- fieldValue.length === 0)
480
- ) {
478
+ const isNoncompliantlyNullish =
479
+ RelayFeatureFlags.ENABLE_NONCOMPLIANT_ERROR_HANDLING_ON_LISTS &&
480
+ Array.isArray(fieldValue) &&
481
+ fieldValue.length === 0;
482
+ if (fieldValue == null || isNoncompliantlyNullish) {
481
483
  if (fieldValue === undefined) {
482
484
  // Fields may be missing in the response in two main cases:
483
485
  // - Inside a client extension: the server will not generally return
@@ -511,20 +513,27 @@ class RelayResponseNormalizer {
511
513
  return;
512
514
  }
513
515
  }
514
- if (__DEV__) {
515
- if (selection.kind === 'ScalarField') {
516
- this._validateConflictingFieldsWithIdenticalId(
517
- record,
518
- storageKey,
519
- // When using `treatMissingFieldsAsNull` the conflicting validation raises a false positive
520
- // because the value is set using `null` but validated using `fieldValue` which at this point
521
- // will be `undefined`.
522
- // Setting this to `null` matches the value that we actually set to the `fieldValue`.
523
- null,
524
- );
516
+ if (selection.kind === 'ScalarField') {
517
+ this._validateConflictingFieldsWithIdenticalId(
518
+ record,
519
+ storageKey,
520
+ // When using `treatMissingFieldsAsNull` the conflicting validation raises a false positive
521
+ // because the value is set using `null` but validated using `fieldValue` which at this point
522
+ // will be `undefined`.
523
+ // Setting this to `null` matches the value that we actually set to the `fieldValue`.
524
+ null,
525
+ );
526
+ }
527
+ if (isNoncompliantlyNullish) {
528
+ // We need to preserve the fact that we received an empty list
529
+ if (selection.kind === 'LinkedField') {
530
+ RelayModernRecord.setLinkedRecordIDs(record, storageKey, []);
531
+ } else {
532
+ RelayModernRecord.setValue(record, storageKey, []);
525
533
  }
534
+ } else {
535
+ RelayModernRecord.setValue(record, storageKey, null);
526
536
  }
527
- RelayModernRecord.setValue(record, storageKey, null);
528
537
  const errorTrie = this._errorTrie;
529
538
  if (errorTrie != null) {
530
539
  const errors = getErrorsByKey(errorTrie, responseKey);
@@ -536,13 +545,11 @@ class RelayResponseNormalizer {
536
545
  }
537
546
 
538
547
  if (selection.kind === 'ScalarField') {
539
- if (__DEV__) {
540
- this._validateConflictingFieldsWithIdenticalId(
541
- record,
542
- storageKey,
543
- fieldValue,
544
- );
545
- }
548
+ this._validateConflictingFieldsWithIdenticalId(
549
+ record,
550
+ storageKey,
551
+ fieldValue,
552
+ );
546
553
  RelayModernRecord.setValue(record, storageKey, fieldValue);
547
554
  } else if (selection.kind === 'LinkedField') {
548
555
  this._path.push(responseKey);
@@ -686,13 +693,11 @@ class RelayResponseNormalizer {
686
693
  'RelayResponseNormalizer: Expected id on field `%s` to be a string.',
687
694
  storageKey,
688
695
  );
689
- if (__DEV__) {
690
- this._validateConflictingLinkedFieldsWithIdenticalId(
691
- RelayModernRecord.getLinkedRecordID(record, storageKey),
692
- nextID,
693
- storageKey,
694
- );
695
- }
696
+ this._validateConflictingLinkedFieldsWithIdenticalId(
697
+ RelayModernRecord.getLinkedRecordID(record, storageKey),
698
+ nextID,
699
+ storageKey,
700
+ );
696
701
  RelayModernRecord.setLinkedRecordID(record, storageKey, nextID);
697
702
  let nextRecord = this._recordSource.get(nextID);
698
703
  if (!nextRecord) {
@@ -700,7 +705,7 @@ class RelayResponseNormalizer {
700
705
  const typeName = field.concreteType || this._getRecordType(fieldValue);
701
706
  nextRecord = RelayModernRecord.create(nextID, typeName);
702
707
  this._recordSource.set(nextID, nextRecord);
703
- } else if (__DEV__) {
708
+ } else {
704
709
  this._validateRecordType(nextRecord, field, fieldValue);
705
710
  }
706
711
  // $FlowFixMe[incompatible-variance]
@@ -766,19 +771,15 @@ class RelayResponseNormalizer {
766
771
  const typeName = field.concreteType || this._getRecordType(item);
767
772
  nextRecord = RelayModernRecord.create(nextID, typeName);
768
773
  this._recordSource.set(nextID, nextRecord);
769
- } else if (__DEV__) {
774
+ } else {
770
775
  this._validateRecordType(nextRecord, field, item);
771
776
  }
772
- // NOTE: the check to strip __DEV__ code only works for simple
773
- // `if (__DEV__)`
774
- if (__DEV__) {
775
- if (prevIDs) {
776
- this._validateConflictingLinkedFieldsWithIdenticalId(
777
- prevIDs[nextIndex],
778
- nextID,
779
- storageKey,
780
- );
781
- }
777
+ if (prevIDs) {
778
+ this._validateConflictingLinkedFieldsWithIdenticalId(
779
+ prevIDs[nextIndex],
780
+ nextID,
781
+ storageKey,
782
+ );
782
783
  }
783
784
  // $FlowFixMe[incompatible-variance]
784
785
  this._traverseSelections(field, nextRecord, item);
@@ -796,20 +797,42 @@ class RelayResponseNormalizer {
796
797
  field: NormalizationLinkedField,
797
798
  payload: Object,
798
799
  ): void {
799
- const typeName = field.concreteType ?? this._getRecordType(payload);
800
- const dataID = RelayModernRecord.getDataID(record);
801
- warning(
802
- (isClientID(dataID) && dataID !== ROOT_ID) ||
803
- RelayModernRecord.getType(record) === typeName,
804
- 'RelayResponseNormalizer: Invalid record `%s`. Expected %s to be ' +
805
- 'consistent, but the record was assigned conflicting types `%s` ' +
806
- 'and `%s`. The GraphQL server likely violated the globally unique ' +
807
- 'id requirement by returning the same id for different objects.',
808
- dataID,
809
- TYPENAME_KEY,
810
- RelayModernRecord.getType(record),
811
- typeName,
812
- );
800
+ if (RelayFeatureFlags.ENABLE_STORE_ID_COLLISION_LOGGING) {
801
+ const typeName = field.concreteType ?? this._getRecordType(payload);
802
+ const dataID = RelayModernRecord.getDataID(record);
803
+ const expected =
804
+ (isClientID(dataID) && dataID !== ROOT_ID) ||
805
+ RelayModernRecord.getType(record) === typeName;
806
+ if (!expected) {
807
+ const logEvent: IdCollisionTypenameLogEvent = {
808
+ name: 'idCollision.typename',
809
+ previous_typename: RelayModernRecord.getType(record),
810
+ new_typename: typeName,
811
+ };
812
+ if (this._log != null) {
813
+ this._log(logEvent);
814
+ }
815
+ }
816
+ }
817
+ // NOTE: Only emit a warning in DEV
818
+ if (__DEV__) {
819
+ const typeName = field.concreteType ?? this._getRecordType(payload);
820
+ const dataID = RelayModernRecord.getDataID(record);
821
+ const expected =
822
+ (isClientID(dataID) && dataID !== ROOT_ID) ||
823
+ RelayModernRecord.getType(record) === typeName;
824
+ warning(
825
+ expected,
826
+ 'RelayResponseNormalizer: Invalid record `%s`. Expected %s to be ' +
827
+ 'consistent, but the record was assigned conflicting types `%s` ' +
828
+ 'and `%s`. The GraphQL server likely violated the globally unique ' +
829
+ 'id requirement by returning the same id for different objects.',
830
+ dataID,
831
+ TYPENAME_KEY,
832
+ RelayModernRecord.getType(record),
833
+ typeName,
834
+ );
835
+ }
813
836
  }
814
837
 
815
838
  /**
@@ -820,14 +843,16 @@ class RelayResponseNormalizer {
820
843
  storageKey: string,
821
844
  fieldValue: mixed,
822
845
  ): void {
823
- // NOTE: Only call this function in DEV
846
+ // NOTE: Only emit a warning in DEV
824
847
  if (__DEV__) {
848
+ const previousValue = RelayModernRecord.getValue(record, storageKey);
825
849
  const dataID = RelayModernRecord.getDataID(record);
826
- var previousValue = RelayModernRecord.getValue(record, storageKey);
827
- warning(
850
+ const expected =
828
851
  storageKey === TYPENAME_KEY ||
829
- previousValue === undefined ||
830
- areEqual(previousValue, fieldValue),
852
+ previousValue === undefined ||
853
+ areEqual(previousValue, fieldValue);
854
+ warning(
855
+ expected,
831
856
  'RelayResponseNormalizer: Invalid record. The record contains two ' +
832
857
  'instances of the same id: `%s` with conflicting field, %s and its values: %s and %s. ' +
833
858
  'If two fields are different but share ' +
@@ -848,10 +873,11 @@ class RelayResponseNormalizer {
848
873
  nextID: DataID,
849
874
  storageKey: string,
850
875
  ): void {
851
- // NOTE: Only call this function in DEV
876
+ // NOTE: Only emit a warning in DEV
852
877
  if (__DEV__) {
878
+ const expected = prevID === undefined || prevID === nextID;
853
879
  warning(
854
- prevID === undefined || prevID === nextID,
880
+ expected,
855
881
  'RelayResponseNormalizer: Invalid record. The record contains ' +
856
882
  'references to the conflicting field, %s and its id values: %s and %s. ' +
857
883
  'We need to make sure that the record the field points ' +
@@ -115,7 +115,7 @@ class RelayStoreSubscriptions implements StoreSubscriptions {
115
115
  missingLiveResolverFields: backup.missingLiveResolverFields,
116
116
  seenRecords: backup.seenRecords,
117
117
  selector: backup.selector,
118
- errorResponseFields: backup.errorResponseFields,
118
+ fieldErrors: backup.fieldErrors,
119
119
  };
120
120
  } else {
121
121
  // This subscription was created during the optimisitic state. We should
@@ -185,7 +185,7 @@ class RelayStoreSubscriptions implements StoreSubscriptions {
185
185
  missingLiveResolverFields: nextSnapshot.missingLiveResolverFields,
186
186
  seenRecords: nextSnapshot.seenRecords,
187
187
  selector: nextSnapshot.selector,
188
- errorResponseFields: nextSnapshot.errorResponseFields,
188
+ fieldErrors: nextSnapshot.fieldErrors,
189
189
  }: Snapshot);
190
190
  if (__DEV__) {
191
191
  deepFreeze(nextSnapshot);
@@ -116,7 +116,7 @@ export type NormalizationSelector = {
116
116
  +variables: Variables,
117
117
  };
118
118
 
119
- export type ErrorResponseField =
119
+ export type FieldError =
120
120
  | RelayFieldPayloadErrorEvent
121
121
  | MissingExpectedDataLogEvent
122
122
  | MissingExpectedDataThrowEvent
@@ -124,7 +124,7 @@ export type ErrorResponseField =
124
124
  | MissingRequiredFieldLogEvent
125
125
  | MissingRequiredFieldThrowEvent;
126
126
 
127
- export type ErrorResponseFields = Array<ErrorResponseField>;
127
+ export type FieldErrors = Array<FieldError>;
128
128
 
129
129
  export type ClientEdgeTraversalInfo = {
130
130
  +readerClientEdge: ReaderClientEdgeToServerObject,
@@ -149,7 +149,7 @@ export type Snapshot = {
149
149
  +missingClientEdges: null | $ReadOnlyArray<MissingClientEdgeRequestInfo>,
150
150
  +seenRecords: DataIDSet,
151
151
  +selector: SingularReaderSelector,
152
- +errorResponseFields: ?ErrorResponseFields,
152
+ +fieldErrors: ?FieldErrors,
153
153
  };
154
154
 
155
155
  /**
@@ -684,6 +684,11 @@ export type ExecuteCompleteLogEvent = {
684
684
  +executeId: number,
685
685
  };
686
686
 
687
+ export type ExecuteUnsubscribeLogEvent = {
688
+ +name: 'execute.unsubscribe',
689
+ +executeId: number,
690
+ };
691
+
687
692
  export type ExecuteNormalizeStart = {
688
693
  +name: 'execute.normalize.start',
689
694
  +operation: OperationDescriptor,
@@ -781,12 +786,23 @@ export type UseFragmentSubscriptionMissedUpdates = {
781
786
  +hasDataChanges: boolean,
782
787
  };
783
788
 
789
+ /**
790
+ * This event is logged when two strong objects share the same id,
791
+ * but have different types, resulting in an collision in the store.
792
+ */
793
+ export type IdCollisionTypenameLogEvent = {
794
+ +name: 'idCollision.typename',
795
+ +previous_typename: string,
796
+ +new_typename: string,
797
+ };
798
+
784
799
  export type LogEvent =
785
800
  | SuspenseFragmentLogEvent
786
801
  | SuspenseQueryLogEvent
787
802
  | QueryResourceFetchLogEvent
788
803
  | QueryResourceRetainLogEvent
789
804
  | FragmentResourceMissingDataLogEvent
805
+ | IdCollisionTypenameLogEvent
790
806
  | PendingOperationFoundLogEvent
791
807
  | NetworkInfoLogEvent
792
808
  | NetworkStartLogEvent
@@ -800,6 +816,7 @@ export type LogEvent =
800
816
  | ExecuteAsyncModuleLogEvent
801
817
  | ExecuteErrorLogEvent
802
818
  | ExecuteCompleteLogEvent
819
+ | ExecuteUnsubscribeLogEvent
803
820
  | ExecuteNormalizeStart
804
821
  | ExecuteNormalizeEnd
805
822
  | StoreDataCheckerStartEvent
@@ -1270,6 +1287,8 @@ export type MissingExpectedDataLogEvent = {
1270
1287
  +kind: 'missing_expected_data.log',
1271
1288
  +owner: string,
1272
1289
  fieldPath: string, // Purposefully mutable to allow lazy construction in RelayReader
1290
+ // To populate this, you should pass the value to a ReactRelayLoggingContext
1291
+ +uiContext: mixed | void,
1273
1292
  };
1274
1293
 
1275
1294
  /**
@@ -1297,6 +1316,8 @@ export type MissingExpectedDataThrowEvent = {
1297
1316
  +owner: string,
1298
1317
  fieldPath: string, // Purposefully mutable to allow lazy construction in RelayReader
1299
1318
  +handled: boolean,
1319
+ // To populate this, you should pass the value to a ReactRelayLoggingContext
1320
+ +uiContext: mixed | void,
1300
1321
  };
1301
1322
 
1302
1323
  /**
@@ -1307,6 +1328,8 @@ export type MissingRequiredFieldLogEvent = {
1307
1328
  +kind: 'missing_required_field.log',
1308
1329
  +owner: string,
1309
1330
  fieldPath: string, // Purposefully mutable to allow lazy construction in RelayReader
1331
+ // To populate this, you should pass the value to a ReactRelayLoggingContext
1332
+ +uiContext: mixed | void,
1310
1333
  };
1311
1334
 
1312
1335
  /**
@@ -1325,6 +1348,8 @@ export type MissingRequiredFieldThrowEvent = {
1325
1348
  +owner: string,
1326
1349
  fieldPath: string, // Purposefully mutable to allow lazy construction in RelayReader
1327
1350
  +handled: boolean,
1351
+ // To populate this, you should pass the value to a ReactRelayLoggingContext
1352
+ +uiContext: mixed | void,
1328
1353
  };
1329
1354
 
1330
1355
  /**
@@ -1346,6 +1371,8 @@ export type RelayResolverErrorEvent = {
1346
1371
  +error: Error,
1347
1372
  +shouldThrow: boolean,
1348
1373
  +handled: boolean,
1374
+ // To populate this, you should pass the value to a ReactRelayLoggingContext
1375
+ +uiContext: mixed | void,
1349
1376
  };
1350
1377
 
1351
1378
  /**
@@ -1372,6 +1399,8 @@ export type RelayFieldPayloadErrorEvent = {
1372
1399
  +error: TRelayFieldError,
1373
1400
  +shouldThrow: boolean,
1374
1401
  +handled: boolean,
1402
+ // To populate this, you should pass the value to a ReactRelayLoggingContext
1403
+ +uiContext: mixed | void,
1375
1404
  };
1376
1405
 
1377
1406
  /**
@@ -1481,7 +1510,7 @@ export type ConcreteClientEdgeResolverReturnType<T = any> = {
1481
1510
  * returns a callback which should be called when the value _may_ have changed.
1482
1511
  *
1483
1512
  * While over-notification (subscription notifications when the read value has
1484
- * not actually changed) is suported, for performance reasons, it is recommended
1513
+ * not actually changed) is supported, for performance reasons, it is recommended
1485
1514
  * that the provider of the LiveState value confirms that the value has indeed
1486
1515
  * change before notifying Relay of the change.
1487
1516
  */
@@ -15,6 +15,8 @@ import type {
15
15
  NormalizationArgument,
16
16
  NormalizationField,
17
17
  NormalizationHandle,
18
+ NormalizationLiveResolverField,
19
+ NormalizationResolverField,
18
20
  } from '../util/NormalizationNode';
19
21
  import type {
20
22
  ReaderActorChange,
@@ -28,6 +30,7 @@ import type {Variables} from '../util/RelayRuntimeTypes';
28
30
 
29
31
  const getRelayHandleKey = require('../util/getRelayHandleKey');
30
32
  const RelayConcreteNode = require('../util/RelayConcreteNode');
33
+ const RelayFeatureFlags = require('../util/RelayFeatureFlags');
31
34
  const {stableCopy} = require('../util/stableCopy');
32
35
  const invariant = require('invariant');
33
36
 
@@ -42,6 +45,8 @@ const ERRORS_KEY: '__errors' = '__errors';
42
45
  const MODULE_COMPONENT_KEY_PREFIX = '__module_component_';
43
46
  const MODULE_OPERATION_KEY_PREFIX = '__module_operation_';
44
47
 
48
+ const RELAY_READ_TIME_RESOLVER_KEY_PREFIX = '$r:';
49
+
45
50
  function getArgumentValue(
46
51
  arg: NormalizationArgument | ReaderArgument,
47
52
  variables: Variables,
@@ -163,6 +168,28 @@ function getStorageKey(
163
168
  : name;
164
169
  }
165
170
 
171
+ /**
172
+ * This is a special case of getStorageKey that should be used when dealing with
173
+ * read time resolver fields. A resolver may be used at both exec time and at read
174
+ * time within the same project. However, the value of the read time resolver is
175
+ * wrapped while the value of the exec time resolver is a standard Relay object. To
176
+ * disambiguate in the case that both types may exist on the same record, the read
177
+ * time resolver storage keys are prefixed.
178
+ */
179
+ function getReadTimeResolverStorageKey(
180
+ field:
181
+ | ReaderRelayResolver
182
+ | ReaderRelayLiveResolver
183
+ | NormalizationResolverField
184
+ | NormalizationLiveResolverField,
185
+ variables: Variables,
186
+ ): string {
187
+ const storageKey = getStorageKey(field, variables);
188
+ return RelayFeatureFlags.ENABLE_READ_TIME_RESOLVER_STORAGE_KEY_PREFIX
189
+ ? '$r:' + storageKey // Using inlined string to test the performance impact
190
+ : storageKey;
191
+ }
192
+
166
193
  /**
167
194
  * Given a field the method returns an array of arguments.
168
195
  * For Relay resolver fields, we store arguments on the field and fragment
@@ -271,12 +298,14 @@ const RelayStoreUtils = {
271
298
  RELAY_RESOLVER_SNAPSHOT_KEY: '__resolverSnapshot',
272
299
  RELAY_RESOLVER_ERROR_KEY: '__resolverError',
273
300
  RELAY_RESOLVER_OUTPUT_TYPE_RECORD_IDS: '__resolverOutputTypeRecordIDs',
301
+ RELAY_READ_TIME_RESOLVER_KEY_PREFIX,
274
302
 
275
303
  formatStorageKey,
276
304
  getArgumentValue,
277
305
  getArgumentValues,
278
306
  getHandleStorageKey,
279
307
  getStorageKey,
308
+ getReadTimeResolverStorageKey,
280
309
  getStableStorageKey,
281
310
  getModuleComponentKey,
282
311
  getModuleOperationKey,
@@ -18,7 +18,7 @@ import type {
18
18
  import type {DataID, Variables} from '../util/RelayRuntimeTypes';
19
19
  import type {
20
20
  DataIDSet,
21
- ErrorResponseFields,
21
+ FieldErrors,
22
22
  SingularReaderSelector,
23
23
  Snapshot,
24
24
  } from './RelayStoreTypes';
@@ -35,7 +35,7 @@ export type EvaluationResult<T> = {
35
35
  export type ResolverFragmentResult = {
36
36
  data: mixed,
37
37
  isMissingData: boolean,
38
- errorResponseFields: ?ErrorResponseFields,
38
+ fieldErrors: ?FieldErrors,
39
39
  };
40
40
 
41
41
  export type GetDataForResolverFragmentFn =
@@ -112,12 +112,14 @@ function readFragment(
112
112
  fragmentSelector.kind === 'SingularReaderSelector',
113
113
  `Expected a singular reader selector for the fragment of the resolver ${fragmentNode.name}, but it was plural.`,
114
114
  );
115
- const {data, isMissingData, errorResponseFields} =
116
- context.getDataForResolverFragment(fragmentSelector, fragmentKey);
115
+ const {data, isMissingData, fieldErrors} = context.getDataForResolverFragment(
116
+ fragmentSelector,
117
+ fragmentKey,
118
+ );
117
119
 
118
120
  if (
119
121
  isMissingData ||
120
- (errorResponseFields != null && errorResponseFields.some(eventShouldThrow))
122
+ (fieldErrors != null && fieldErrors.some(eventShouldThrow))
121
123
  ) {
122
124
  throw RESOLVER_FRAGMENT_ERRORED_SENTINEL;
123
125
  }
@@ -32,11 +32,14 @@ if (__DEV__) {
32
32
  }
33
33
  formattersInstalled = true;
34
34
  // $FlowFixMe[incompatible-use] D61394600
35
+ // $FlowFixMe[cannot-resolve-name]
35
36
  if (window.devtoolsFormatters == null) {
36
37
  // $FlowFixMe[incompatible-use] D61394600
38
+ // $FlowFixMe[cannot-resolve-name]
37
39
  window.devtoolsFormatters = [];
38
40
  }
39
41
  // $FlowFixMe[incompatible-use] D61394600
42
+ // $FlowFixMe[cannot-resolve-name]
40
43
  if (!Array.isArray(window.devtoolsFormatters)) {
41
44
  return;
42
45
  }
@@ -47,6 +50,7 @@ if (__DEV__) {
47
50
  'section.',
48
51
  );
49
52
  // $FlowFixMe[incompatible-use] D61394600
53
+ // $FlowFixMe[cannot-resolve-name]
50
54
  window.devtoolsFormatters.push(...createFormatters());
51
55
  };
52
56
 
@@ -137,6 +141,7 @@ if (__DEV__) {
137
141
  ): ?{[string]: mixed} => {
138
142
  const record = source.get(dataID);
139
143
  if (record == null) {
144
+ // $FlowFixMe[incompatible-return]
140
145
  return record;
141
146
  }
142
147
  return new Proxy(
@@ -12,6 +12,7 @@
12
12
  'use strict';
13
13
 
14
14
  import type {RelayContext} from './RelayStoreTypes.js';
15
+ import type {Context} from 'react';
15
16
  import typeof {createContext} from 'react';
16
17
 
17
18
  const invariant = require('invariant');
@@ -24,10 +25,10 @@ type React = $ReadOnly<{
24
25
  ...
25
26
  }>;
26
27
 
27
- let relayContext: ?React$Context<RelayContext | null>;
28
+ let relayContext: ?Context<RelayContext | null>;
28
29
  let firstReact: ?React;
29
30
 
30
- function createRelayContext(react: React): React$Context<RelayContext | null> {
31
+ function createRelayContext(react: React): Context<RelayContext | null> {
31
32
  if (!relayContext) {
32
33
  relayContext = react.createContext(null);
33
34
  if (__DEV__) {
@@ -0,0 +1,46 @@
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
+ * @format
9
+ * @oncall relay
10
+ */
11
+
12
+ 'use strict';
13
+
14
+ import type {Context} from 'react';
15
+ import typeof {createContext} from 'react';
16
+
17
+ const invariant = require('invariant');
18
+
19
+ // Ideally, we'd just import the type of the react module, but this causes Flow
20
+ // problems.
21
+ type React = $ReadOnly<{
22
+ createContext: createContext<mixed | null>,
23
+ version: string,
24
+ ...
25
+ }>;
26
+
27
+ let relayLoggingContext: ?Context<mixed | null>;
28
+ let firstReact: ?React;
29
+
30
+ function createRelayLoggingContext(react: React): Context<mixed | null> {
31
+ if (!relayLoggingContext) {
32
+ relayLoggingContext = react.createContext(null);
33
+ if (__DEV__) {
34
+ relayLoggingContext.displayName = 'RelayLoggingContext';
35
+ }
36
+ firstReact = react;
37
+ }
38
+ invariant(
39
+ react === firstReact,
40
+ '[createRelayLoggingContext]: You are passing a different instance of React',
41
+ react.version,
42
+ );
43
+ return relayLoggingContext;
44
+ }
45
+
46
+ module.exports = createRelayLoggingContext;
@@ -0,0 +1,25 @@
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
+ * @format
9
+ * @oncall relay
10
+ */
11
+
12
+ 'use strict';
13
+
14
+ import type {DataID} from 'relay-runtime/util/RelayRuntimeTypes';
15
+
16
+ const TYPENAME_PREFIX = '__type:';
17
+
18
+ function generateTypenamePrefixedDataID(
19
+ typeName: string,
20
+ dataID: DataID,
21
+ ): DataID {
22
+ return `${TYPENAME_PREFIX}${typeName}:${dataID}`;
23
+ }
24
+
25
+ module.exports = {generateTypenamePrefixedDataID};
@@ -48,6 +48,7 @@ const {
48
48
  RELAY_RESOLVER_OUTPUT_TYPE_RECORD_IDS,
49
49
  RELAY_RESOLVER_SNAPSHOT_KEY,
50
50
  RELAY_RESOLVER_VALUE_KEY,
51
+ getReadTimeResolverStorageKey,
51
52
  getStorageKey,
52
53
  } = require('../RelayStoreUtils');
53
54
  const getOutputTypeRecordIDs = require('./getOutputTypeRecordIDs');
@@ -129,7 +130,7 @@ class LiveResolverCache implements ResolverCache {
129
130
  // resolvers on this parent record.
130
131
  const record = expectRecord(recordSource, recordID);
131
132
 
132
- const storageKey = getStorageKey(field, variables);
133
+ const storageKey = getReadTimeResolverStorageKey(field, variables);
133
134
  let linkedID = RelayModernRecord.getLinkedRecordID(record, storageKey);
134
135
  let linkedRecord = linkedID == null ? null : recordSource.get(linkedID);
135
136