relay-runtime 18.1.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 (92) hide show
  1. package/experimental.js +1 -1
  2. package/experimental.js.flow +22 -9
  3. package/handlers/connection/ConnectionHandler.js.flow +6 -1
  4. package/index.js +1 -1
  5. package/index.js.flow +4 -0
  6. package/lib/experimental.js +5 -2
  7. package/lib/handlers/connection/ConnectionHandler.js +1 -1
  8. package/lib/index.js +3 -0
  9. package/lib/multi-actor-environment/ActorSpecificEnvironment.js +1 -1
  10. package/lib/mutations/RelayRecordProxy.js +14 -3
  11. package/lib/mutations/RelayRecordSourceMutator.js +17 -0
  12. package/lib/mutations/RelayRecordSourceProxy.js +2 -1
  13. package/lib/mutations/createUpdatableProxy.js +1 -1
  14. package/lib/mutations/validateMutation.js +2 -2
  15. package/lib/network/RelayObservable.js +1 -3
  16. package/lib/network/wrapNetworkWithLogObserver.js +2 -2
  17. package/lib/query/fetchQuery.js +1 -1
  18. package/lib/store/DataChecker.js +4 -5
  19. package/lib/store/OperationExecutor.js +11 -0
  20. package/lib/store/RelayModernEnvironment.js +13 -4
  21. package/lib/store/RelayModernFragmentSpecResolver.js +4 -4
  22. package/lib/store/RelayModernStore.js +43 -21
  23. package/lib/store/RelayPublishQueue.js +11 -15
  24. package/lib/store/RelayReader.js +244 -183
  25. package/lib/store/RelayReferenceMarker.js +3 -4
  26. package/lib/store/RelayResponseNormalizer.js +48 -26
  27. package/lib/store/RelayStoreSubscriptions.js +2 -2
  28. package/lib/store/RelayStoreUtils.js +8 -0
  29. package/lib/store/ResolverCache.js +1 -165
  30. package/lib/store/ResolverFragments.js +2 -2
  31. package/lib/store/createRelayLoggingContext.js +17 -0
  32. package/lib/store/generateTypenamePrefixedDataID.js +9 -0
  33. package/lib/store/live-resolvers/LiveResolverCache.js +5 -10
  34. package/lib/store/live-resolvers/resolverDataInjector.js +4 -4
  35. package/lib/store/observeFragmentExperimental.js +60 -13
  36. package/lib/store/observeQueryExperimental.js +21 -0
  37. package/lib/util/RelayFeatureFlags.js +7 -2
  38. package/lib/util/handlePotentialSnapshotErrors.js +12 -9
  39. package/multi-actor-environment/ActorSpecificEnvironment.js.flow +1 -0
  40. package/mutations/RelayRecordProxy.js.flow +30 -3
  41. package/mutations/RelayRecordSourceMutator.js.flow +27 -0
  42. package/mutations/RelayRecordSourceProxy.js.flow +4 -0
  43. package/mutations/createUpdatableProxy.js.flow +1 -1
  44. package/mutations/validateMutation.js.flow +3 -3
  45. package/network/RelayNetworkTypes.js.flow +3 -0
  46. package/network/RelayObservable.js.flow +1 -5
  47. package/network/wrapNetworkWithLogObserver.js.flow +19 -1
  48. package/package.json +1 -1
  49. package/query/fetchQuery.js.flow +1 -1
  50. package/store/DataChecker.js.flow +5 -2
  51. package/store/OperationExecutor.js.flow +12 -1
  52. package/store/RelayExperimentalGraphResponseTransform.js.flow +4 -4
  53. package/store/RelayModernEnvironment.js.flow +22 -6
  54. package/store/RelayModernFragmentSpecResolver.js.flow +6 -6
  55. package/store/RelayModernRecord.js.flow +1 -1
  56. package/store/RelayModernSelector.js.flow +2 -0
  57. package/store/RelayModernStore.js.flow +74 -27
  58. package/store/RelayOptimisticRecordSource.js.flow +2 -0
  59. package/store/RelayPublishQueue.js.flow +32 -21
  60. package/store/RelayReader.js.flow +400 -145
  61. package/store/RelayRecordState.js.flow +1 -1
  62. package/store/RelayReferenceMarker.js.flow +3 -4
  63. package/store/RelayResponseNormalizer.js.flow +94 -62
  64. package/store/RelayStoreSubscriptions.js.flow +2 -2
  65. package/store/RelayStoreTypes.js.flow +45 -15
  66. package/store/RelayStoreUtils.js.flow +30 -1
  67. package/store/ResolverCache.js.flow +2 -271
  68. package/store/ResolverFragments.js.flow +5 -3
  69. package/store/StoreInspector.js.flow +5 -0
  70. package/store/createRelayContext.js.flow +3 -2
  71. package/store/createRelayLoggingContext.js.flow +46 -0
  72. package/store/generateTypenamePrefixedDataID.js.flow +25 -0
  73. package/store/live-resolvers/LiveResolverCache.js.flow +5 -10
  74. package/store/live-resolvers/resolverDataInjector.js.flow +10 -6
  75. package/store/observeFragmentExperimental.js.flow +82 -28
  76. package/store/observeQueryExperimental.js.flow +61 -0
  77. package/store/waitForFragmentExperimental.js.flow +4 -3
  78. package/util/NormalizationNode.js.flow +10 -1
  79. package/util/ReaderNode.js.flow +9 -3
  80. package/util/RelayConcreteNode.js.flow +3 -1
  81. package/util/RelayError.js.flow +1 -0
  82. package/util/RelayFeatureFlags.js.flow +31 -7
  83. package/util/RelayRuntimeTypes.js.flow +17 -3
  84. package/util/getPaginationVariables.js.flow +2 -0
  85. package/util/handlePotentialSnapshotErrors.js.flow +24 -12
  86. package/util/registerEnvironmentWithDevTools.js.flow +4 -2
  87. package/util/withProvidedVariables.js.flow +1 -0
  88. package/util/withStartAndDuration.js.flow +3 -0
  89. package/relay-runtime-experimental.js +0 -4
  90. package/relay-runtime-experimental.min.js +0 -9
  91. package/relay-runtime.js +0 -4
  92. package/relay-runtime.min.js +0 -9
@@ -11,8 +11,11 @@
11
11
 
12
12
  'use strict';
13
13
 
14
+ import type {Result} from '../experimental';
14
15
  import type {
16
+ CatchFieldTo,
15
17
  ReaderActorChange,
18
+ ReaderAliasedInlineFragmentSpread,
16
19
  ReaderCatchField,
17
20
  ReaderClientEdge,
18
21
  ReaderFragment,
@@ -32,10 +35,9 @@ import type {DataID, Variables} from '../util/RelayRuntimeTypes';
32
35
  import type {
33
36
  ClientEdgeTraversalInfo,
34
37
  DataIDSet,
35
- ErrorResponseField,
36
- ErrorResponseFields,
38
+ FieldError,
39
+ FieldErrors,
37
40
  MissingClientEdgeRequestInfo,
38
- MissingLiveResolverField,
39
41
  Record,
40
42
  RecordSource,
41
43
  RequestDescriptor,
@@ -47,6 +49,7 @@ import type {
47
49
  import type {Arguments} from './RelayStoreUtils';
48
50
  import type {EvaluationResult, ResolverCache} from './ResolverCache';
49
51
 
52
+ const RelayFeatureFlags = require('../util/RelayFeatureFlags');
50
53
  const {
51
54
  isSuspenseSentinel,
52
55
  } = require('./live-resolvers/LiveResolverSuspenseSentinel');
@@ -72,8 +75,6 @@ const {
72
75
  const {generateTypeID} = require('./TypeID');
73
76
  const invariant = require('invariant');
74
77
 
75
- type RequiredOrCatchField = ReaderRequiredField | ReaderCatchField;
76
-
77
78
  function read(
78
79
  recordSource: RecordSource,
79
80
  selector: SingularReaderSelector,
@@ -96,10 +97,16 @@ class RelayReader {
96
97
  _clientEdgeTraversalPath: Array<ClientEdgeTraversalInfo | null>;
97
98
  _isMissingData: boolean;
98
99
  _missingClientEdges: Array<MissingClientEdgeRequestInfo>;
99
- _missingLiveResolverFields: Array<MissingLiveResolverField>;
100
+ _missingLiveResolverFields: Array<DataID>;
100
101
  _isWithinUnmatchedTypeRefinement: boolean;
101
- _errorResponseFields: ?ErrorResponseFields;
102
+ _fieldErrors: ?FieldErrors;
102
103
  _owner: RequestDescriptor;
104
+ // Exec time resolvers are run before reaching the Relay store so the store already contains
105
+ // the normalized data; the same as if the data were sent from the server. However, since a
106
+ // resolver could be used at read time or exec time in different queries, the reader AST for
107
+ // a resolver is the read time AST. At runtime, this flag is used to ignore the extra
108
+ // information in the read time resolver AST and use the "standard", non-resolver read paths
109
+ _useExecTimeResolvers: boolean;
103
110
  _recordSource: RecordSource;
104
111
  _seenRecords: DataIDSet;
105
112
  _updatedDataIDs: DataIDSet;
@@ -122,8 +129,13 @@ class RelayReader {
122
129
  this._missingLiveResolverFields = [];
123
130
  this._isMissingData = false;
124
131
  this._isWithinUnmatchedTypeRefinement = false;
125
- this._errorResponseFields = null;
132
+ this._fieldErrors = null;
126
133
  this._owner = selector.owner;
134
+ this._useExecTimeResolvers =
135
+ this._owner.node.operation.use_exec_time_resolvers ??
136
+ this._owner.node.operation.exec_time_resolvers_enabled_provider?.get() ===
137
+ true ??
138
+ false;
127
139
  this._recordSource = recordSource;
128
140
  this._seenRecords = new Set();
129
141
  this._selector = selector;
@@ -175,7 +187,14 @@ class RelayReader {
175
187
  }
176
188
 
177
189
  this._isWithinUnmatchedTypeRefinement = !isDataExpectedToBePresent;
178
- const data = this._traverse(node, dataID, null);
190
+ let data = this._traverse(node, dataID, null);
191
+
192
+ // If the fragment/operation was marked with @catch, we need to handle any
193
+ // errors that were encountered while reading the fields within it.
194
+ const catchTo = this._selector.node.metadata?.catchTo;
195
+ if (catchTo != null) {
196
+ data = this._catchErrors(data, catchTo, null) as $FlowFixMe;
197
+ }
179
198
 
180
199
  if (this._updatedDataIDs.size > 0) {
181
200
  this._resolverCache.notifyUpdatedSubscribers(this._updatedDataIDs);
@@ -190,11 +209,11 @@ class RelayReader {
190
209
  missingLiveResolverFields: this._missingLiveResolverFields,
191
210
  seenRecords: this._seenRecords,
192
211
  selector: this._selector,
193
- errorResponseFields: this._errorResponseFields,
212
+ fieldErrors: this._fieldErrors,
194
213
  };
195
214
  }
196
215
 
197
- _maybeAddErrorResponseFields(record: Record, storageKey: string): void {
216
+ _maybeAddFieldErrors(record: Record, storageKey: string): void {
198
217
  const errors = RelayModernRecord.getErrors(record, storageKey);
199
218
 
200
219
  if (errors == null) {
@@ -202,42 +221,55 @@ class RelayReader {
202
221
  }
203
222
  const owner = this._fragmentName;
204
223
 
205
- if (this._errorResponseFields == null) {
206
- this._errorResponseFields = [];
224
+ if (this._fieldErrors == null) {
225
+ this._fieldErrors = [];
207
226
  }
208
- for (const error of errors) {
209
- this._errorResponseFields.push({
227
+ for (let i = 0; i < errors.length; i++) {
228
+ const error = errors[i];
229
+ this._fieldErrors.push({
210
230
  kind: 'relay_field_payload.error',
211
231
  owner,
212
232
  fieldPath: (error.path ?? []).join('.'),
213
233
  error,
214
234
  shouldThrow: this._selector.node.metadata?.throwOnFieldError ?? false,
215
235
  handled: false,
236
+ // the uiContext is always undefined here.
237
+ // the loggingContext is provided by hooks - and assigned to uiContext in handlePotentialSnapshotErrors
238
+ uiContext: undefined,
216
239
  });
217
240
  }
218
241
  }
219
242
 
220
- _markDataAsMissing(): void {
243
+ _markDataAsMissing(fieldName: string): void {
221
244
  if (this._isWithinUnmatchedTypeRefinement) {
222
245
  return;
223
246
  }
224
- if (this._errorResponseFields == null) {
225
- this._errorResponseFields = [];
247
+ if (this._fieldErrors == null) {
248
+ this._fieldErrors = [];
226
249
  }
227
250
 
228
251
  // we will add the path later
229
- const fieldPath = '';
230
252
  const owner = this._fragmentName;
231
253
 
232
- this._errorResponseFields.push(
254
+ this._fieldErrors.push(
233
255
  this._selector.node.metadata?.throwOnFieldError ?? false
234
256
  ? {
235
257
  kind: 'missing_expected_data.throw',
236
258
  owner,
237
- fieldPath,
259
+ fieldPath: fieldName,
238
260
  handled: false,
261
+ // the uiContext is always undefined here.
262
+ // the loggingContext is provided by hooks - and assigned to uiContext in handlePotentialSnapshotErrors
263
+ uiContext: undefined,
239
264
  }
240
- : {kind: 'missing_expected_data.log', owner, fieldPath},
265
+ : {
266
+ kind: 'missing_expected_data.log',
267
+ owner,
268
+ fieldPath: fieldName,
269
+ // the uiContext is always undefined here.
270
+ // the loggingContext is provided by hooks - and assigned to uiContext in handlePotentialSnapshotErrors
271
+ uiContext: undefined,
272
+ },
241
273
  );
242
274
 
243
275
  this._isMissingData = true;
@@ -266,8 +298,9 @@ class RelayReader {
266
298
  this._seenRecords.add(dataID);
267
299
  if (record == null) {
268
300
  if (record === undefined) {
269
- this._markDataAsMissing();
301
+ this._markDataAsMissing('<record>');
270
302
  }
303
+ // $FlowFixMe[incompatible-return]
271
304
  return record;
272
305
  }
273
306
  const data = prevData || {};
@@ -288,53 +321,139 @@ class RelayReader {
288
321
  return this._variables[name];
289
322
  }
290
323
 
291
- _maybeReportUnexpectedNull(fieldPath: string, action: 'LOG' | 'THROW') {
324
+ _maybeReportUnexpectedNull(selection: ReaderRequiredField) {
325
+ if (selection.action === 'NONE') {
326
+ return;
327
+ }
292
328
  const owner = this._fragmentName;
293
329
 
294
- if (this._errorResponseFields == null) {
295
- this._errorResponseFields = [];
330
+ if (this._fieldErrors == null) {
331
+ this._fieldErrors = [];
296
332
  }
297
333
 
298
- switch (action) {
334
+ let fieldName: string;
335
+ if (selection.field.linkedField != null) {
336
+ fieldName =
337
+ selection.field.linkedField.alias ?? selection.field.linkedField.name;
338
+ } else {
339
+ fieldName = selection.field.alias ?? selection.field.name;
340
+ }
341
+
342
+ switch (selection.action) {
299
343
  case 'THROW':
300
- this._errorResponseFields.push({
344
+ this._fieldErrors.push({
301
345
  kind: 'missing_required_field.throw',
302
- fieldPath,
346
+ fieldPath: fieldName,
303
347
  owner,
304
348
  handled: false,
349
+ // the uiContext is always undefined here.
350
+ // the loggingContext is provided by hooks - and assigned to uiContext in handlePotentialSnapshotErrors
351
+ uiContext: undefined,
305
352
  });
306
353
  return;
307
354
  case 'LOG':
308
- this._errorResponseFields.push({
355
+ this._fieldErrors.push({
309
356
  kind: 'missing_required_field.log',
310
- fieldPath,
357
+ fieldPath: fieldName,
311
358
  owner,
359
+ // the uiContext is always undefined here.
360
+ // the loggingContext is provided by hooks - and assigned to uiContext in handlePotentialSnapshotErrors
361
+ uiContext: undefined,
312
362
  });
313
363
  return;
314
364
  default:
315
- (action: empty);
365
+ (selection.action: empty);
316
366
  }
317
367
  }
318
368
 
319
- _handleCatchToResult(
320
- selection: ReaderCatchField,
321
- record: Record,
322
- data: SelectorData,
369
+ _handleRequiredFieldValue(
370
+ selection: ReaderRequiredField,
323
371
  value: mixed,
324
- ) {
325
- const field = selection.field?.backingField ?? selection.field;
326
- const fieldName = field?.alias ?? field?.name;
372
+ ): boolean /*should continue to siblings*/ {
373
+ if (value == null) {
374
+ this._maybeReportUnexpectedNull(selection);
375
+ // We are going to throw, or our parent is going to get nulled out.
376
+ // Either way, sibling values are going to be ignored, so we can
377
+ // bail early here as an optimization.
378
+ return false;
379
+ }
380
+ return true;
381
+ }
327
382
 
328
- // ReaderClientExtension doesn't have `alias` or `name`
329
- // so we don't support this yet
330
- invariant(
331
- fieldName != null,
332
- "Couldn't determine field name for this field. It might be a ReaderClientExtension - which is not yet supported.",
333
- );
383
+ /**
384
+ * Fields, aliased inline fragments, fragments and operations with `@catch`
385
+ * directives must handle the case that errors were encountered while reading
386
+ * any fields within them.
387
+ *
388
+ * 1. Before traversing into the selection(s) marked as `@catch`, the caller
389
+ * stores the previous field errors (`this._fieldErrors`) in a
390
+ * variable.
391
+ * 2. After traversing into the selection(s) marked as `@catch`, the caller
392
+ * calls this method with the resulting value, the `to` value from the
393
+ * `@catch` directive, and the previous field errors.
394
+ *
395
+ * This method will then:
396
+ *
397
+ * 1. Compute the correct value to return based on any errors encountered and the supplied `to` type.
398
+ * 2. Mark any errors encountered within the `@catch` as "handled" to ensure they don't cause the reader to throw.
399
+ * 3. Merge any errors encountered within the `@catch` with the previous field errors.
400
+ */
401
+ _catchErrors<T>(
402
+ _value: T,
403
+ to: CatchFieldTo,
404
+ previousResponseFields: ?FieldErrors,
405
+ ): ?T | Result<T, mixed> {
406
+ let value: T | null | Result<T, mixed> = _value;
407
+ switch (to) {
408
+ case 'RESULT':
409
+ value = this._asResult(_value);
410
+ break;
411
+ case 'NULL':
412
+ if (this._fieldErrors != null && this._fieldErrors.length > 0) {
413
+ value = null;
414
+ }
415
+ break;
416
+ default:
417
+ (to: empty);
418
+ }
419
+
420
+ const childrenFieldErrors = this._fieldErrors;
421
+
422
+ this._fieldErrors = previousResponseFields;
423
+
424
+ // Merge any errors encountered within the @catch with the previous field
425
+ // errors, but mark them as "handled" first.
426
+ if (childrenFieldErrors != null) {
427
+ if (this._fieldErrors == null) {
428
+ this._fieldErrors = [];
429
+ }
430
+ for (let i = 0; i < childrenFieldErrors.length; i++) {
431
+ // We mark any errors encountered within the @catch as "handled"
432
+ // to ensure that they don't cause the reader to throw, but can
433
+ // still be logged.
434
+ this._fieldErrors.push(
435
+ markFieldErrorHasHandled(childrenFieldErrors[i]),
436
+ );
437
+ }
438
+ }
439
+ return value;
440
+ }
441
+
442
+ /**
443
+ * Convert a value into a Result object based on the presence of errors in the
444
+ * `this._fieldErrors` array.
445
+ *
446
+ * **Note**: This method does _not_ mark errors as handled. It is the caller's
447
+ * responsibility to ensure that errors are marked as handled.
448
+ */
449
+ _asResult<T>(value: T): Result<T, mixed> {
450
+ if (this._fieldErrors == null || this._fieldErrors.length === 0) {
451
+ return {ok: true, value};
452
+ }
334
453
 
335
454
  // TODO: Should we be hiding log level events here?
336
- const errors = this._errorResponseFields
337
- ?.map(error => {
455
+ const errors = this._fieldErrors
456
+ .map(error => {
338
457
  switch (error.kind) {
339
458
  case 'relay_field_payload.error':
340
459
  const {message, ...displayError} = error.error;
@@ -361,31 +480,14 @@ class RelayReader {
361
480
  (error.kind: empty);
362
481
  invariant(
363
482
  false,
364
- 'Unexpected error errorResponseField kind: %s',
483
+ 'Unexpected error fieldError kind: %s',
365
484
  error.kind,
366
485
  );
367
486
  }
368
487
  })
369
488
  .filter(Boolean);
370
489
 
371
- data[fieldName] = errors != null ? {ok: false, errors} : {ok: true, value};
372
- }
373
-
374
- _handleRequiredFieldValue(
375
- selection: ReaderRequiredField,
376
- value: mixed,
377
- ): boolean /*should continue to siblings*/ {
378
- if (value == null) {
379
- const {action} = selection;
380
- if (action !== 'NONE') {
381
- this._maybeReportUnexpectedNull(selection.path, action);
382
- }
383
- // We are going to throw, or our parent is going to get nulled out.
384
- // Either way, sibling values are going to be ignored, so we can
385
- // bail early here as an optimization.
386
- return false;
387
- }
388
- return true;
490
+ return {ok: false, errors};
389
491
  }
390
492
 
391
493
  _traverseSelections(
@@ -408,9 +510,9 @@ class RelayReader {
408
510
  }
409
511
  break;
410
512
  case 'CatchField': {
411
- const previousResponseFields = this._errorResponseFields;
513
+ const previousResponseFields = this._fieldErrors;
412
514
 
413
- this._errorResponseFields = null;
515
+ this._fieldErrors = null;
414
516
 
415
517
  const catchFieldValue = this._readClientSideDirectiveField(
416
518
  selection,
@@ -418,27 +520,20 @@ class RelayReader {
418
520
  data,
419
521
  );
420
522
 
421
- if (selection.to === 'RESULT') {
422
- this._handleCatchToResult(selection, record, data, catchFieldValue);
423
- }
424
-
425
- const childrenErrorResponseFields = this._errorResponseFields;
426
-
427
- this._errorResponseFields = previousResponseFields;
523
+ const field = selection.field?.backingField ?? selection.field;
524
+ const fieldName = field?.alias ?? field?.name;
525
+ // ReaderClientExtension doesn't have `alias` or `name`
526
+ // so we don't support this yet
527
+ invariant(
528
+ fieldName != null,
529
+ "Couldn't determine field name for this field. It might be a ReaderClientExtension - which is not yet supported.",
530
+ );
428
531
 
429
- if (childrenErrorResponseFields != null) {
430
- if (this._errorResponseFields == null) {
431
- this._errorResponseFields = [];
432
- }
433
- for (let i = 0; i < childrenErrorResponseFields.length; i++) {
434
- // We mark any errors encountered within the @catch as "handled"
435
- // to ensure that they don't cause the reader to throw, but can
436
- // still be logged.
437
- this._errorResponseFields.push(
438
- markFieldErrorHasHandled(childrenErrorResponseFields[i]),
439
- );
440
- }
441
- }
532
+ data[fieldName] = this._catchErrors(
533
+ catchFieldValue,
534
+ selection.to,
535
+ previousResponseFields,
536
+ );
442
537
 
443
538
  break;
444
539
  }
@@ -482,23 +577,18 @@ class RelayReader {
482
577
  }
483
578
  case 'RelayLiveResolver':
484
579
  case 'RelayResolver': {
485
- this._readResolverField(selection, record, data);
580
+ if (this._useExecTimeResolvers) {
581
+ this._readScalar(selection, record, data);
582
+ } else {
583
+ this._readResolverField(selection, record, data);
584
+ }
486
585
  break;
487
586
  }
488
587
  case 'FragmentSpread':
489
588
  this._createFragmentPointer(selection, record, data);
490
589
  break;
491
590
  case 'AliasedInlineFragmentSpread': {
492
- let fieldValue = this._readInlineFragment(
493
- selection.fragment,
494
- record,
495
- {},
496
- true,
497
- );
498
- if (fieldValue === false) {
499
- fieldValue = null;
500
- }
501
- data[selection.name] = fieldValue;
591
+ this._readAliasedInlineFragment(selection, record, data);
502
592
  break;
503
593
  }
504
594
  case 'ModuleImport':
@@ -550,7 +640,20 @@ class RelayReader {
550
640
  break;
551
641
  case 'ClientEdgeToClientObject':
552
642
  case 'ClientEdgeToServerObject':
553
- this._readClientEdge(selection, record, data);
643
+ if (
644
+ this._useExecTimeResolvers &&
645
+ (selection.backingField.kind === 'RelayResolver' ||
646
+ selection.backingField.kind === 'RelayLiveResolver')
647
+ ) {
648
+ const {linkedField} = selection;
649
+ if (linkedField.plural) {
650
+ this._readPluralLink(linkedField, record, data);
651
+ } else {
652
+ this._readLink(linkedField, record, data);
653
+ }
654
+ } else {
655
+ this._readClientEdge(selection, record, data);
656
+ }
554
657
  break;
555
658
  default:
556
659
  (selection: empty);
@@ -565,7 +668,7 @@ class RelayReader {
565
668
  }
566
669
 
567
670
  _readClientSideDirectiveField(
568
- selection: RequiredOrCatchField,
671
+ selection: ReaderRequiredField | ReaderCatchField,
569
672
  record: Record,
570
673
  data: SelectorData,
571
674
  ): ?mixed {
@@ -578,19 +681,39 @@ class RelayReader {
578
681
  } else {
579
682
  return this._readLink(selection.field, record, data);
580
683
  }
684
+
581
685
  case 'RelayResolver':
582
- return this._readResolverField(selection.field, record, data);
583
- case 'RelayLiveResolver':
584
- return this._readResolverField(selection.field, record, data);
686
+ case 'RelayLiveResolver': {
687
+ if (this._useExecTimeResolvers) {
688
+ return this._readScalar(selection.field, record, data);
689
+ } else {
690
+ return this._readResolverField(selection.field, record, data);
691
+ }
692
+ }
585
693
  case 'ClientEdgeToClientObject':
586
694
  case 'ClientEdgeToServerObject':
587
- return this._readClientEdge(selection.field, record, data);
695
+ if (
696
+ this._useExecTimeResolvers &&
697
+ (selection.field.backingField.kind === 'RelayResolver' ||
698
+ selection.field.backingField.kind === 'RelayLiveResolver')
699
+ ) {
700
+ const {field} = selection;
701
+ if (field.linkedField.plural) {
702
+ return this._readPluralLink(field.linkedField, record, data);
703
+ } else {
704
+ return this._readLink(field.linkedField, record, data);
705
+ }
706
+ } else {
707
+ return this._readClientEdge(selection.field, record, data);
708
+ }
709
+ case 'AliasedInlineFragmentSpread':
710
+ return this._readAliasedInlineFragment(selection.field, record, data);
588
711
  default:
589
712
  (selection.field.kind: empty);
590
713
  invariant(
591
714
  false,
592
715
  'RelayReader(): Unexpected ast kind `%s`.',
593
- selection.kind,
716
+ selection.field.kind,
594
717
  );
595
718
  }
596
719
  }
@@ -601,9 +724,12 @@ class RelayReader {
601
724
  data: SelectorData,
602
725
  ): mixed {
603
726
  const parentRecordID = RelayModernRecord.getDataID(record);
727
+ const prevErrors = this._fieldErrors;
728
+ this._fieldErrors = null;
604
729
  const result = this._readResolverFieldImpl(field, parentRecordID);
605
730
 
606
731
  const fieldName = field.alias ?? field.name;
732
+ this._prependPreviousErrors(prevErrors, fieldName);
607
733
  data[fieldName] = result;
608
734
  return result;
609
735
  }
@@ -637,7 +763,7 @@ class RelayReader {
637
763
  return {
638
764
  data: snapshot.data,
639
765
  isMissingData: snapshot.isMissingData,
640
- errorResponseFields: snapshot.errorResponseFields,
766
+ fieldErrors: snapshot.fieldErrors,
641
767
  };
642
768
  }
643
769
 
@@ -650,7 +776,7 @@ class RelayReader {
650
776
  return {
651
777
  data: snapshot.data,
652
778
  isMissingData: snapshot.isMissingData,
653
- errorResponseFields: snapshot.errorResponseFields,
779
+ fieldErrors: snapshot.fieldErrors,
654
780
  };
655
781
  };
656
782
 
@@ -735,7 +861,8 @@ class RelayReader {
735
861
  // upwards to mimic the behavior of having traversed into that fragment directly.
736
862
  if (cachedSnapshot != null) {
737
863
  if (cachedSnapshot.missingClientEdges != null) {
738
- for (const missing of cachedSnapshot.missingClientEdges) {
864
+ for (let i = 0; i < cachedSnapshot.missingClientEdges.length; i++) {
865
+ const missing = cachedSnapshot.missingClientEdges[i];
739
866
  this._missingClientEdges.push(missing);
740
867
  }
741
868
  }
@@ -744,27 +871,34 @@ class RelayReader {
744
871
  this._isMissingData ||
745
872
  cachedSnapshot.missingLiveResolverFields.length > 0;
746
873
 
747
- for (const missingResolverField of cachedSnapshot.missingLiveResolverFields) {
874
+ for (
875
+ let i = 0;
876
+ i < cachedSnapshot.missingLiveResolverFields.length;
877
+ i++
878
+ ) {
879
+ const missingResolverField =
880
+ cachedSnapshot.missingLiveResolverFields[i];
748
881
  this._missingLiveResolverFields.push(missingResolverField);
749
882
  }
750
883
  }
751
- if (cachedSnapshot.errorResponseFields != null) {
752
- if (this._errorResponseFields == null) {
753
- this._errorResponseFields = [];
884
+ if (cachedSnapshot.fieldErrors != null) {
885
+ if (this._fieldErrors == null) {
886
+ this._fieldErrors = [];
754
887
  }
755
- for (const error of cachedSnapshot.errorResponseFields) {
888
+ for (let i = 0; i < cachedSnapshot.fieldErrors.length; i++) {
889
+ const error = cachedSnapshot.fieldErrors[i];
756
890
  if (this._selector.node.metadata?.throwOnFieldError === true) {
757
891
  // If this fragment is @throwOnFieldError, any destructive error
758
892
  // encountered inside a resolver's fragment is equivilent to the
759
893
  // resolver field having a field error, and we want that to cause this
760
894
  // fragment to throw. So, we propagate all errors as is.
761
- this._errorResponseFields.push(error);
895
+ this._fieldErrors.push(error);
762
896
  } else {
763
897
  // If this fragment is _not_ @throwOnFieldError, we will simply
764
898
  // accept that any destructive errors encountered in the resolver's
765
899
  // root fragment will cause the resolver to return null, and well
766
900
  // pass the errors along to the logger marked as "handled".
767
- this._errorResponseFields.push(markFieldErrorHasHandled(error));
901
+ this._fieldErrors.push(markFieldErrorHasHandled(error));
768
902
  }
769
903
  }
770
904
  }
@@ -775,18 +909,21 @@ class RelayReader {
775
909
  // the errors can be attached to this read's snapshot. This allows the error
776
910
  // to be logged.
777
911
  if (resolverError) {
778
- const errorEvent = {
912
+ const errorEvent: FieldError = {
779
913
  kind: 'relay_resolver.error',
780
914
  fieldPath,
781
915
  owner: this._fragmentName,
782
916
  error: resolverError,
783
917
  shouldThrow: this._selector.node.metadata?.throwOnFieldError ?? false,
784
918
  handled: false,
919
+ // the uiContext is always undefined here.
920
+ // the loggingContext is provided by hooks - and assigned to uiContext in handlePotentialSnapshotErrors
921
+ uiContext: undefined,
785
922
  };
786
- if (this._errorResponseFields == null) {
787
- this._errorResponseFields = [errorEvent];
923
+ if (this._fieldErrors == null) {
924
+ this._fieldErrors = [errorEvent];
788
925
  } else {
789
- this._errorResponseFields.push(errorEvent);
926
+ this._fieldErrors.push(errorEvent);
790
927
  }
791
928
  }
792
929
 
@@ -804,15 +941,14 @@ class RelayReader {
804
941
  // they know when to unsuspend.
805
942
  if (suspenseID != null) {
806
943
  this._isMissingData = true;
807
- this._missingLiveResolverFields.push({
808
- path: `${this._fragmentName}.${fieldPath}`,
809
- liveStateID: suspenseID,
810
- });
944
+ this._missingLiveResolverFields.push(suspenseID);
811
945
  }
812
946
  if (updatedDataIDs != null) {
813
- for (const recordID of updatedDataIDs) {
947
+ // Iterating a Set with for of is okay
948
+ // eslint-disable-next-line relay-internal/no-for-of-loops
949
+ updatedDataIDs.forEach(recordID => {
814
950
  this._updatedDataIDs.add(recordID);
815
- }
951
+ });
816
952
  }
817
953
  }
818
954
 
@@ -982,12 +1118,15 @@ class RelayReader {
982
1118
  RelayModernRecord.getDataID(record),
983
1119
  prevData,
984
1120
  );
1121
+ const prevErrors = this._fieldErrors;
1122
+ this._fieldErrors = null;
985
1123
  const edgeValue = this._traverse(
986
1124
  field.linkedField,
987
1125
  storeID,
988
1126
  // $FlowFixMe[incompatible-variance]
989
1127
  prevData,
990
1128
  );
1129
+ this._prependPreviousErrors(prevErrors, fieldName);
991
1130
  this._clientEdgeTraversalPath.pop();
992
1131
  data[fieldName] = edgeValue;
993
1132
  return edgeValue;
@@ -995,17 +1134,22 @@ class RelayReader {
995
1134
  }
996
1135
 
997
1136
  _readScalar(
998
- field: ReaderScalarField,
1137
+ field: ReaderScalarField | ReaderRelayResolver | ReaderRelayLiveResolver,
999
1138
  record: Record,
1000
1139
  data: SelectorData,
1001
1140
  ): ?mixed {
1002
1141
  const fieldName = field.alias ?? field.name;
1003
1142
  const storageKey = getStorageKey(field, this._variables);
1004
1143
  const value = RelayModernRecord.getValue(record, storageKey);
1005
- if (value === null) {
1006
- this._maybeAddErrorResponseFields(record, storageKey);
1144
+ if (
1145
+ value === null ||
1146
+ (RelayFeatureFlags.ENABLE_NONCOMPLIANT_ERROR_HANDLING_ON_LISTS &&
1147
+ Array.isArray(value) &&
1148
+ value.length === 0)
1149
+ ) {
1150
+ this._maybeAddFieldErrors(record, storageKey);
1007
1151
  } else if (value === undefined) {
1008
- this._markDataAsMissing();
1152
+ this._markDataAsMissing(fieldName);
1009
1153
  }
1010
1154
  data[fieldName] = value;
1011
1155
  return value;
@@ -1022,9 +1166,9 @@ class RelayReader {
1022
1166
  if (linkedID == null) {
1023
1167
  data[fieldName] = linkedID;
1024
1168
  if (linkedID === null) {
1025
- this._maybeAddErrorResponseFields(record, storageKey);
1169
+ this._maybeAddFieldErrors(record, storageKey);
1026
1170
  } else if (linkedID === undefined) {
1027
- this._markDataAsMissing();
1171
+ this._markDataAsMissing(fieldName);
1028
1172
  }
1029
1173
  return linkedID;
1030
1174
  }
@@ -1039,12 +1183,76 @@ class RelayReader {
1039
1183
  RelayModernRecord.getDataID(record),
1040
1184
  prevData,
1041
1185
  );
1186
+ const prevErrors = this._fieldErrors;
1187
+ this._fieldErrors = null;
1042
1188
  // $FlowFixMe[incompatible-variance]
1043
1189
  const value = this._traverse(field, linkedID, prevData);
1190
+
1191
+ this._prependPreviousErrors(prevErrors, fieldName);
1044
1192
  data[fieldName] = value;
1045
1193
  return value;
1046
1194
  }
1047
1195
 
1196
+ /**
1197
+ * Adds a set of field errors to `this._fieldErrors`, ensuring the
1198
+ * `fieldPath` property of existing field errors are prefixed with the given
1199
+ * `fieldNameOrIndex`.
1200
+ *
1201
+ * In order to make field errors maximally useful in logs/errors, we want to
1202
+ * include the path to the field that caused the error. A naive approach would
1203
+ * be to maintain a path property on RelayReader which we push/pop field names
1204
+ * to as we traverse into fields/etc. However, this would be expensive to
1205
+ * maintain, and in the common case where there are no field errors, the work
1206
+ * would go unused.
1207
+ *
1208
+ * Instead, we take a lazy approach where as we exit the recurison into a
1209
+ * field/etc we prepend any errors encountered while traversing that field
1210
+ * with the field name. This is somewhat more expensive in the error case, but
1211
+ * ~free in the common case where there are no errors.
1212
+ *
1213
+ * To achieve this, named field readers must do the following to correctly
1214
+ * track error filePaths:
1215
+ *
1216
+ * 1. Stash the value of `this._fieldErrors` in a local variable
1217
+ * 2. Set `this._fieldErrors` to `null`
1218
+ * 3. Traverse into the field
1219
+ * 4. Call this method with the stashed errors and the field's name
1220
+ *
1221
+ * Similarly, when creating field errors, we simply initialize the `fieldPath`
1222
+ * as the direct field name.
1223
+ *
1224
+ * Today we only use this apporach for `missing_expected_data` and
1225
+ * `missing_required_field` errors, but we intend to broaden it to handle all
1226
+ * field error paths.
1227
+ */
1228
+ _prependPreviousErrors(
1229
+ prevErrors: ?Array<FieldError>,
1230
+ fieldNameOrIndex: string | number,
1231
+ ): void {
1232
+ if (this._fieldErrors != null) {
1233
+ for (let i = 0; i < this._fieldErrors.length; i++) {
1234
+ const event = this._fieldErrors[i];
1235
+ if (
1236
+ event.owner === this._fragmentName &&
1237
+ (event.kind === 'missing_expected_data.throw' ||
1238
+ event.kind === 'missing_expected_data.log' ||
1239
+ event.kind === 'missing_required_field.throw' ||
1240
+ event.kind === 'missing_required_field.log')
1241
+ ) {
1242
+ event.fieldPath = `${fieldNameOrIndex}.${event.fieldPath}`;
1243
+ }
1244
+ }
1245
+ if (prevErrors != null) {
1246
+ for (let i = this._fieldErrors.length - 1; i >= 0; i--) {
1247
+ prevErrors.push(this._fieldErrors[i]);
1248
+ }
1249
+ this._fieldErrors = prevErrors;
1250
+ }
1251
+ } else {
1252
+ this._fieldErrors = prevErrors;
1253
+ }
1254
+ }
1255
+
1048
1256
  _readActorChange(
1049
1257
  field: ReaderActorChange,
1050
1258
  record: Record,
@@ -1060,9 +1268,9 @@ class RelayReader {
1060
1268
  if (externalRef == null) {
1061
1269
  data[fieldName] = externalRef;
1062
1270
  if (externalRef === undefined) {
1063
- this._markDataAsMissing();
1271
+ this._markDataAsMissing(fieldName);
1064
1272
  } else if (externalRef === null) {
1065
- this._maybeAddErrorResponseFields(record, storageKey);
1273
+ this._maybeAddFieldErrors(record, storageKey);
1066
1274
  }
1067
1275
  return data[fieldName];
1068
1276
  }
@@ -1090,8 +1298,13 @@ class RelayReader {
1090
1298
  ): ?mixed {
1091
1299
  const storageKey = getStorageKey(field, this._variables);
1092
1300
  const linkedIDs = RelayModernRecord.getLinkedRecordIDs(record, storageKey);
1093
- if (linkedIDs === null) {
1094
- this._maybeAddErrorResponseFields(record, storageKey);
1301
+ if (
1302
+ linkedIDs === null ||
1303
+ (RelayFeatureFlags.ENABLE_NONCOMPLIANT_ERROR_HANDLING_ON_LISTS &&
1304
+ Array.isArray(linkedIDs) &&
1305
+ linkedIDs.length === 0)
1306
+ ) {
1307
+ this._maybeAddFieldErrors(record, storageKey);
1095
1308
  }
1096
1309
  return this._readLinkedIds(field, linkedIDs, record, data);
1097
1310
  }
@@ -1107,7 +1320,7 @@ class RelayReader {
1107
1320
  if (linkedIDs == null) {
1108
1321
  data[fieldName] = linkedIDs;
1109
1322
  if (linkedIDs === undefined) {
1110
- this._markDataAsMissing();
1323
+ this._markDataAsMissing(fieldName);
1111
1324
  }
1112
1325
  return linkedIDs;
1113
1326
  }
@@ -1121,11 +1334,13 @@ class RelayReader {
1121
1334
  RelayModernRecord.getDataID(record),
1122
1335
  prevData,
1123
1336
  );
1337
+ const prevErrors = this._fieldErrors;
1338
+ this._fieldErrors = null;
1124
1339
  const linkedArray = prevData || [];
1125
1340
  linkedIDs.forEach((linkedID, nextIndex) => {
1126
1341
  if (linkedID == null) {
1127
1342
  if (linkedID === undefined) {
1128
- this._markDataAsMissing();
1343
+ this._markDataAsMissing(String(nextIndex));
1129
1344
  }
1130
1345
  // $FlowFixMe[cannot-write]
1131
1346
  linkedArray[nextIndex] = linkedID;
@@ -1140,10 +1355,14 @@ class RelayReader {
1140
1355
  RelayModernRecord.getDataID(record),
1141
1356
  prevItem,
1142
1357
  );
1358
+ const prevErrors = this._fieldErrors;
1359
+ this._fieldErrors = null;
1143
1360
  // $FlowFixMe[cannot-write]
1144
1361
  // $FlowFixMe[incompatible-variance]
1145
1362
  linkedArray[nextIndex] = this._traverse(field, linkedID, prevItem);
1363
+ this._prependPreviousErrors(prevErrors, nextIndex);
1146
1364
  });
1365
+ this._prependPreviousErrors(prevErrors, fieldName);
1147
1366
  data[fieldName] = linkedArray;
1148
1367
  return linkedArray;
1149
1368
  }
@@ -1160,13 +1379,18 @@ class RelayReader {
1160
1379
  // Determine the component module from the store: if the field is missing
1161
1380
  // it means we don't know what component to render the match with.
1162
1381
  const componentKey = getModuleComponentKey(moduleImport.documentName);
1382
+ const relayStoreComponent = RelayModernRecord.getValue(
1383
+ record,
1384
+ componentKey,
1385
+ );
1163
1386
  // componentModuleProvider is used by Client 3D for read time resolvers.
1164
1387
  const component =
1165
- moduleImport.componentModuleProvider ??
1166
- RelayModernRecord.getValue(record, componentKey);
1388
+ relayStoreComponent !== undefined
1389
+ ? relayStoreComponent
1390
+ : moduleImport.componentModuleProvider;
1167
1391
  if (component == null) {
1168
1392
  if (component === undefined) {
1169
- this._markDataAsMissing();
1393
+ this._markDataAsMissing('<module-import>');
1170
1394
  }
1171
1395
  return;
1172
1396
  }
@@ -1189,6 +1413,39 @@ class RelayReader {
1189
1413
  data[MODULE_COMPONENT_KEY] = component;
1190
1414
  }
1191
1415
 
1416
+ /**
1417
+ * Aliased inline fragments allow the user to check if the data in an inline
1418
+ * fragment was fetched. Data in the inline fragment can be conditional in the
1419
+ * case of a type condition on the inline fragment or directives like `@skip`
1420
+ * or `@include`.
1421
+ *
1422
+ * We model aliased inline fragments as a special reader node wrapped around a
1423
+ * regular inline fragment reader node.
1424
+ *
1425
+ * This allows us to read the inline fragment as normal, check if it matched,
1426
+ * and then define the alias to either contain the inline fragment's data, or
1427
+ * null.
1428
+ */
1429
+ _readAliasedInlineFragment(
1430
+ aliasedInlineFragment: ReaderAliasedInlineFragmentSpread,
1431
+ record: Record,
1432
+ data: SelectorData,
1433
+ ) {
1434
+ const prevErrors = this._fieldErrors;
1435
+ this._fieldErrors = null;
1436
+ let fieldValue = this._readInlineFragment(
1437
+ aliasedInlineFragment.fragment,
1438
+ record,
1439
+ {},
1440
+ true,
1441
+ );
1442
+ this._prependPreviousErrors(prevErrors, aliasedInlineFragment.name);
1443
+ if (fieldValue === false) {
1444
+ fieldValue = null;
1445
+ }
1446
+ data[aliasedInlineFragment.name] = fieldValue;
1447
+ }
1448
+
1192
1449
  // Has three possible return values:
1193
1450
  // * null: The type condition did not match
1194
1451
  // * undefined: We are missing data
@@ -1398,16 +1655,14 @@ class RelayReader {
1398
1655
  // fetched the `__is[AbstractType]` flag for this concrete type. In this
1399
1656
  // case we need to report that we are missing data, in case that field is
1400
1657
  // still in flight.
1401
- this._markDataAsMissing();
1658
+ this._markDataAsMissing('<abstract-type-hint>');
1402
1659
  }
1403
1660
  // $FlowFixMe Casting record value
1404
1661
  return implementsInterface;
1405
1662
  }
1406
1663
  }
1407
1664
 
1408
- function markFieldErrorHasHandled(
1409
- event: ErrorResponseField,
1410
- ): ErrorResponseField {
1665
+ function markFieldErrorHasHandled(event: FieldError): FieldError {
1411
1666
  switch (event.kind) {
1412
1667
  case 'missing_expected_data.throw':
1413
1668
  case 'missing_required_field.throw':