@apollo/federation-internals 2.4.8 → 2.4.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/operations.ts CHANGED
@@ -478,7 +478,7 @@ export class FragmentElement extends AbstractOperationElement<FragmentElement> {
478
478
  // schema.
479
479
  const { canRebase, rebasedCondition } = this.canRebaseOn(parentType);
480
480
  validate(
481
- canRebase,
481
+ canRebase,
482
482
  () => `Cannot add fragment of condition "${typeCondition}" (runtimes: [${possibleRuntimeTypes(typeCondition!)}]) to parent type "${parentType}" (runtimes: ${possibleRuntimeTypes(parentType)})`
483
483
  );
484
484
  return this.withUpdatedTypes(parentType, rebasedCondition);
@@ -697,7 +697,7 @@ export type RootOperationPath = {
697
697
  path: OperationPath
698
698
  }
699
699
 
700
- // Computes for every fragment, which other fragments use it (so the reverse of it's dependencies, the other fragment it uses).
700
+ // Computes for every fragment, which other fragments use it (so the reverse of it's dependencies, the other fragment it uses).
701
701
  function computeFragmentsDependents(fragments: NamedFragments): SetMultiMap<string, string> {
702
702
  const reverseDeps = new SetMultiMap<string, string>();
703
703
  for (const fragment of fragments.definitions()) {
@@ -934,9 +934,17 @@ export class Operation {
934
934
  // leaving this is not a huge deal and it's not worth the complexity, but it could be that we can
935
935
  // refactor all this later to avoid this case without additional complexity.
936
936
  if (finalFragments) {
937
- const usages = new Map<string, number>();
938
- optimizedSelection.collectUsedFragmentNames(usages);
939
- finalFragments = finalFragments.filter((f) => (usages.get(f.name) ?? 0) > 0);
937
+ // Note that removing a fragment might lead to another fragment being unused, so we need to iterate
938
+ // until there is nothing more to remove, or we're out of fragments.
939
+ let beforeRemoval: NamedFragments;
940
+ do {
941
+ beforeRemoval = finalFragments;
942
+ const usages = new Map<string, number>();
943
+ // Collecting all usages, both in the selection and within other fragments.
944
+ optimizedSelection.collectUsedFragmentNames(usages);
945
+ finalFragments.collectUsedFragmentNames(usages);
946
+ finalFragments = finalFragments.filter((f) => (usages.get(f.name) ?? 0) > 0);
947
+ } while (finalFragments && finalFragments.size < beforeRemoval.size);
940
948
  }
941
949
  }
942
950
 
@@ -1098,27 +1106,26 @@ export class NamedFragmentDefinition extends DirectiveTargetElement<NamedFragmen
1098
1106
 
1099
1107
  /**
1100
1108
  * Whether this fragment may apply _directly_ at the provided type, meaning that the fragment sub-selection
1101
- * could be put directly inside a `... on type {}` inline fragment (_without_ re-adding the fragment condition
1102
- * that is), and both be valid and not "lose context".
1109
+ * (_without_ the fragment condition, hence the "directly") can be normalized at `type` and this without
1110
+ * "widening" the runtime types to types that do not intersect the fragment condition.
1103
1111
  *
1104
1112
  * For that to be true, we need one of this to be true:
1105
1113
  * 1. the runtime types of the fragment condition must be at least as general as those of the provided `type`.
1106
- * Otherwise, putting it at `type` without its condition would "generalize" more than fragment meant to (and
1107
- * so we'd "lose context"
1114
+ * Otherwise, putting it at `type` without its condition would "generalize" more than the fragment meant to (and
1115
+ * so we'd "widen" the runtime types more than what the query meant to.
1108
1116
  * 2. either `type` and `this.typeCondition` are equal, or `type` is an object or `this.typeCondition` is a union
1109
1117
  * The idea is that, assuming our 1st point, then:
1110
1118
  * - if both are equal, things works trivially.
1111
1119
  * - if `type` is an object, `this.typeCondition` is either the same object, or a union/interface for which
1112
1120
  * type is a valid runtime. In all case, anything valid on `this.typeCondition` would apply to `type` too.
1113
- * - if `this.typeCondition` is a union, then it's selection can only have fragments on object types at top-level
1114
- * (save for `__typename`), and all those selection will work at `type` too.
1121
+ * - if `this.typeCondition` is a union, then it's selection can only have fragments at top-level
1122
+ * (no fields save for `__typename`), and normalising is always fine with top-level fragments.
1115
1123
  * But in any other case, both types must be abstract (if `this.typeCondition` is an object, the 1st condition
1116
1124
  * imply `type` can only be the same type) and we're in one of:
1117
1125
  * - `type` and `this.typeCondition` are both different interfaces (that intersect but are different).
1118
1126
  * - `type` is aunion and `this.typeCondition` an interface.
1119
- * And in both cases, the selection of the fragment may selection an interface that is not valid at `type` (if `type`
1120
- * is a union because a direct field is always wrong, and if `type` is another interface because that interface may
1121
- * not have that particular field).
1127
+ * And in both cases, since `this.typeCondition` is an interface, the fragment selection set may have field selections
1128
+ * on that interface, and those fields may not be valid for `type`.
1122
1129
  *
1123
1130
  * @param type - the type at which we're looking at applying the fragment
1124
1131
  */
@@ -1274,6 +1281,15 @@ export class NamedFragments {
1274
1281
  return this.fragments.values();
1275
1282
  }
1276
1283
 
1284
+ /**
1285
+ * Collect the usages of fragments that are used within the selection of other fragments.
1286
+ */
1287
+ collectUsedFragmentNames(collector: Map<string, number>) {
1288
+ for (const fragment of this.definitions()) {
1289
+ fragment.collectUsedFragmentNames(collector);
1290
+ }
1291
+ }
1292
+
1277
1293
  map(mapper: (def: NamedFragmentDefinition) => NamedFragmentDefinition): NamedFragments {
1278
1294
  const mapped = new NamedFragments();
1279
1295
  for (const def of this.fragments.values()) {
@@ -1460,7 +1476,7 @@ class DeferNormalizer {
1460
1476
  }
1461
1477
 
1462
1478
  export enum ContainsResult {
1463
- // Note: enum values are numbers in the end, and 0 means false in JS, so we should keep `NOT_CONTAINED` first
1479
+ // Note: enum values are numbers in the end, and 0 means false in JS, so we should keep `NOT_CONTAINED` first
1464
1480
  // so that using the result of `contains` as a boolean works.
1465
1481
  NOT_CONTAINED,
1466
1482
  STRICTLY_CONTAINED,
@@ -1570,7 +1586,7 @@ export class SelectionSet {
1570
1586
  // With that, `optimizeSelections` will correctly match on the `on Query` fragment; after which
1571
1587
  // we can unpack the final result.
1572
1588
  const wrapped = new InlineFragmentSelection(new FragmentElement(this.parentType, this.parentType), this);
1573
- const validator = FieldsConflictValidator.build(this);
1589
+ const validator = FieldsConflictMultiBranchValidator.ofInitial(FieldsConflictValidator.build(this));
1574
1590
  const optimized = wrapped.optimize(fragments, validator);
1575
1591
 
1576
1592
  // Now, it's possible we matched a full fragment, in which case `optimized` will be just the named fragment,
@@ -1584,7 +1600,7 @@ export class SelectionSet {
1584
1600
  // Tries to match fragments inside each selections of this selection set, and this recursively. However, note that this
1585
1601
  // may not match fragments that would apply at top-level, so you should usually use `optimize` instead (this exists mostly
1586
1602
  // for the recursion).
1587
- optimizeSelections(fragments: NamedFragments, validator: FieldsConflictValidator): SelectionSet {
1603
+ optimizeSelections(fragments: NamedFragments, validator: FieldsConflictMultiBranchValidator): SelectionSet {
1588
1604
  return this.lazyMap((selection) => selection.optimize(fragments, validator));
1589
1605
  }
1590
1606
 
@@ -1593,7 +1609,7 @@ export class SelectionSet {
1593
1609
  }
1594
1610
 
1595
1611
  /**
1596
- * Applies some normalization rules to this selection set in the context of the provided `parentType`.
1612
+ * Applies some normalization rules to this selection set in the context of the provided `parentType`.
1597
1613
  *
1598
1614
  * Normalization mostly removes unecessary/redundant inline fragments, so that for instance, with
1599
1615
  * schema:
@@ -1657,10 +1673,12 @@ export class SelectionSet {
1657
1673
  * }
1658
1674
  * ```
1659
1675
  *
1660
- * For this operation to be valid (to not throw), `parentType` must be such this selection set would
1661
- * be valid as a subselection of an inline fragment `... on parentType { <this selection set> }` (and
1662
- * so `this.normalize(this.parentType)` is always valid and useful, but it is possible to pass a `parentType`
1663
- * that is more "restrictive" than the selection current parent type).
1676
+ * For this operation to be valid (to not throw), `parentType` must be such that every field selection in
1677
+ * this selection set is such that the field parent type intersects `parentType` (there is no limitation
1678
+ * on the fragment selections, though any fragment selections whose condition do not intersects `parentType`
1679
+ * will be discarded). Note that `this.normalize(this.parentType)` is always valid and useful, but it is
1680
+ * also possible to pass a `parentType` that is more "restrictive" than the selection current parent type
1681
+ * (as long as the top-level fields of this selection set can be rebased on that type).
1664
1682
  *
1665
1683
  * Passing the option `recursive == false` makes the normalization only apply at the top-level, removing
1666
1684
  * any unecessary top-level inline fragments, possibly multiple layers of them, but we never recurse
@@ -1792,6 +1810,11 @@ export class SelectionSet {
1792
1810
  : ContainsResult.STRICTLY_CONTAINED;
1793
1811
  }
1794
1812
 
1813
+ containsTopLevelField(field: Field): boolean {
1814
+ const selection = this._keyedSelections.get(field.key());
1815
+ return !!selection && selection.element.equals(field);
1816
+ }
1817
+
1795
1818
  /**
1796
1819
  * Returns a selection set that correspond to this selection set but where any of the selections in the
1797
1820
  * provided selection set have been remove.
@@ -2192,7 +2215,7 @@ function makeSelectionSet(parentType: CompositeType, keyedUpdates: MultiMap<stri
2192
2215
  }
2193
2216
 
2194
2217
  /**
2195
- * A simple wrapper over a `SelectionSetUpdates` that allows to conveniently build a selection set, then add some more updates and build it again, etc...
2218
+ * A simple wrapper over a `SelectionSetUpdates` that allows to conveniently build a selection set, then add some more updates and build it again, etc...
2196
2219
  */
2197
2220
  export class MutableSelectionSet<TMemoizedValue extends { [key: string]: any } = {}> {
2198
2221
  private computed: SelectionSet | undefined;
@@ -2327,7 +2350,7 @@ abstract class AbstractSelection<TElement extends OperationElement, TIsLeaf exte
2327
2350
 
2328
2351
  abstract key(): string;
2329
2352
 
2330
- abstract optimize(fragments: NamedFragments, validator: FieldsConflictValidator): Selection;
2353
+ abstract optimize(fragments: NamedFragments, validator: FieldsConflictMultiBranchValidator): Selection;
2331
2354
 
2332
2355
  abstract toSelectionNode(): SelectionNode;
2333
2356
 
@@ -2425,7 +2448,7 @@ abstract class AbstractSelection<TElement extends OperationElement, TIsLeaf exte
2425
2448
  parentType: CompositeType,
2426
2449
  subSelection: SelectionSet,
2427
2450
  fragments: NamedFragments,
2428
- validator: FieldsConflictValidator,
2451
+ validator: FieldsConflictMultiBranchValidator,
2429
2452
  canUseFullMatchingFragment: (match: NamedFragmentDefinition) => boolean,
2430
2453
  }): SelectionSet | NamedFragmentDefinition {
2431
2454
  // We limit to fragments whose selection could be applied "directly" at `parentType`, meaning without taking the fragment condition
@@ -2433,7 +2456,7 @@ abstract class AbstractSelection<TElement extends OperationElement, TIsLeaf exte
2433
2456
  // have been "normalized away" and so we want for this very call to be called on the fragment whose type _is_ the fragment condition (at
2434
2457
  // which point, this `maybeApplyingDirectlyAtType` method will apply.
2435
2458
  // Also note that this is because we have this restriction that calling `expandedSelectionSetAtType` is ok.
2436
- let candidates = fragments.maybeApplyingDirectlyAtType(parentType);
2459
+ const candidates = fragments.maybeApplyingDirectlyAtType(parentType);
2437
2460
  if (candidates.length === 0) {
2438
2461
  return subSelection;
2439
2462
  }
@@ -2558,54 +2581,24 @@ abstract class AbstractSelection<TElement extends OperationElement, TIsLeaf exte
2558
2581
  }
2559
2582
  }
2560
2583
 
2561
- class FieldsConflictValidator {
2584
+ class FieldsConflictMultiBranchValidator {
2562
2585
  private usedSpreadTrimmedPartAtLevel?: FieldsConflictValidator[];
2563
2586
 
2564
- private constructor(
2565
- private readonly byResponseName: Map<string, Map<Field, FieldsConflictValidator | null>>,
2587
+ constructor(
2588
+ private readonly validators: FieldsConflictValidator[],
2566
2589
  ) {
2567
2590
  }
2568
2591
 
2569
- static build(s: SelectionSet): FieldsConflictValidator {
2570
- return FieldsConflictValidator.forLevel(s.fieldsInSet());
2592
+ static ofInitial(validator: FieldsConflictValidator): FieldsConflictMultiBranchValidator {
2593
+ return new FieldsConflictMultiBranchValidator([validator]);
2571
2594
  }
2572
2595
 
2573
- private static forLevel(level: CollectedFieldsInSet): FieldsConflictValidator {
2574
- const atLevel = new Map<string, Map<Field, CollectedFieldsInSet | null>>();
2575
-
2576
- for (const { field } of level) {
2577
- const responseName = field.element.responseName();
2578
- let atResponseName = atLevel.get(responseName);
2579
- if (!atResponseName) {
2580
- atResponseName = new Map<Field, CollectedFieldsInSet>();
2581
- atLevel.set(responseName, atResponseName);
2582
- }
2583
- if (field.selectionSet) {
2584
- let forField = atResponseName.get(field.element) ?? [];
2585
- atResponseName.set(field.element, forField.concat(field.selectionSet.fieldsInSet()));
2586
- } else {
2587
- atResponseName.set(field.element, null);
2588
- }
2589
- }
2590
-
2591
- const byResponseName = new Map<string, Map<Field, FieldsConflictValidator | null>>();
2592
- for (const [name, level] of atLevel.entries()) {
2593
- const atResponseName = new Map<Field, FieldsConflictValidator | null>();
2594
- for (const [field, collectedFields] of level) {
2595
- const validator = collectedFields ? FieldsConflictValidator.forLevel(collectedFields) : null;
2596
- atResponseName.set(field, validator);
2597
- }
2598
- byResponseName.set(name, atResponseName);
2599
- }
2600
- return new FieldsConflictValidator(byResponseName);
2601
- }
2602
-
2603
- forField(field: Field): FieldsConflictValidator {
2604
- const validator = this.byResponseName.get(field.responseName())?.get(field);
2605
- // This should be called on validator built on the exact selection set from field this `field` is coming, so
2606
- // we should find it or the code is buggy.
2607
- assert(validator, () => `Should have found validator for ${field}`);
2608
- return validator;
2596
+ forField(field: Field): FieldsConflictMultiBranchValidator {
2597
+ const forAllBranches = this.validators.flatMap((vs) => vs.forField(field));
2598
+ // As this is called on (non-leaf) field from the same query on which we have build the initial validators, we
2599
+ // should find at least one validator.
2600
+ assert(forAllBranches.length > 0, `Shoud have found at least one validator for ${field}`);
2601
+ return new FieldsConflictMultiBranchValidator(forAllBranches);
2609
2602
  }
2610
2603
 
2611
2604
  // At this point, we known that the fragment, restricted to the current parent type, matches a subset of the
@@ -2627,7 +2620,7 @@ class FieldsConflictValidator {
2627
2620
  return true;
2628
2621
  }
2629
2622
 
2630
- if (!this.doMergeWith(validator)) {
2623
+ if (!this.validators.every((v) => v.doMergeWith(validator))) {
2631
2624
  return false;
2632
2625
  }
2633
2626
 
@@ -2654,6 +2647,61 @@ class FieldsConflictValidator {
2654
2647
  this.usedSpreadTrimmedPartAtLevel.push(validator);
2655
2648
  return true;
2656
2649
  }
2650
+ }
2651
+
2652
+ class FieldsConflictValidator {
2653
+ private constructor(
2654
+ private readonly byResponseName: Map<string, Map<Field, FieldsConflictValidator | null>>,
2655
+ ) {
2656
+ }
2657
+
2658
+ static build(s: SelectionSet): FieldsConflictValidator {
2659
+ return FieldsConflictValidator.forLevel(s.fieldsInSet());
2660
+ }
2661
+
2662
+ private static forLevel(level: CollectedFieldsInSet): FieldsConflictValidator {
2663
+ const atLevel = new Map<string, Map<Field, CollectedFieldsInSet | null>>();
2664
+
2665
+ for (const { field } of level) {
2666
+ const responseName = field.element.responseName();
2667
+ let atResponseName = atLevel.get(responseName);
2668
+ if (!atResponseName) {
2669
+ atResponseName = new Map<Field, CollectedFieldsInSet>();
2670
+ atLevel.set(responseName, atResponseName);
2671
+ }
2672
+ if (field.selectionSet) {
2673
+ // It's unlikely that we've seen the same `field.element` as we don't particularly "intern" `Field` object (so even if the exact same field
2674
+ // is used in 2 parts of a selection set, it will probably be a different `Field` object), so the `get` below will probably mostly return `undefined`,
2675
+ // but it wouldn't be incorrect to re-use a `Field` object multiple side, so no reason not to handle that correctly.
2676
+ const forField = atResponseName.get(field.element) ?? [];
2677
+ atResponseName.set(field.element, forField.concat(field.selectionSet.fieldsInSet()));
2678
+ } else {
2679
+ // Note that whether a `FieldSelection` has `selectionSet` or not is entirely determined by whether the field type is a composite type
2680
+ // or not, so even if we've seen a previous version of `field.element` before, we know it's guarantee to have had no `selectionSet`.
2681
+ // So the `set` below may overwrite a previous entry, but it would be a `null` so no harm done.
2682
+ atResponseName.set(field.element, null);
2683
+ }
2684
+ }
2685
+
2686
+ const byResponseName = new Map<string, Map<Field, FieldsConflictValidator | null>>();
2687
+ for (const [name, level] of atLevel.entries()) {
2688
+ const atResponseName = new Map<Field, FieldsConflictValidator | null>();
2689
+ for (const [field, collectedFields] of level) {
2690
+ const validator = collectedFields ? FieldsConflictValidator.forLevel(collectedFields) : null;
2691
+ atResponseName.set(field, validator);
2692
+ }
2693
+ byResponseName.set(name, atResponseName);
2694
+ }
2695
+ return new FieldsConflictValidator(byResponseName);
2696
+ }
2697
+
2698
+ forField(field: Field): FieldsConflictValidator[] {
2699
+ const byResponseName = this.byResponseName.get(field.responseName());
2700
+ if (!byResponseName) {
2701
+ return [];
2702
+ }
2703
+ return mapValues(byResponseName).filter((v): v is FieldsConflictValidator => !!v);
2704
+ }
2657
2705
 
2658
2706
  doMergeWith(that: FieldsConflictValidator): boolean {
2659
2707
  for (const [responseName, thisFields] of this.byResponseName.entries()) {
@@ -2761,7 +2809,7 @@ export class FieldSelection extends AbstractSelection<Field<any>, undefined, Fie
2761
2809
  return this.element.key();
2762
2810
  }
2763
2811
 
2764
- optimize(fragments: NamedFragments, validator: FieldsConflictValidator): Selection {
2812
+ optimize(fragments: NamedFragments, validator: FieldsConflictMultiBranchValidator): Selection {
2765
2813
  const fieldBaseType = baseType(this.element.definition.type!);
2766
2814
  if (!isCompositeType(fieldBaseType) || !this.selectionSet) {
2767
2815
  return this;
@@ -2770,22 +2818,20 @@ export class FieldSelection extends AbstractSelection<Field<any>, undefined, Fie
2770
2818
  const fieldValidator = validator.forField(this.element);
2771
2819
 
2772
2820
  // First, see if we can reuse fragments for the selection of this field.
2773
- let optimizedSelection = this.selectionSet;
2774
- if (isCompositeType(fieldBaseType) && this.selectionSet) {
2775
- const optimized = this.tryOptimizeSubselectionWithFragments({
2776
- parentType: fieldBaseType,
2777
- subSelection: this.selectionSet,
2778
- fragments,
2779
- validator: fieldValidator,
2780
- // We can never apply a fragments that has directives on it at the field level.
2781
- canUseFullMatchingFragment: (fragment) => fragment.appliedDirectives.length === 0,
2782
- });
2821
+ const optimized = this.tryOptimizeSubselectionWithFragments({
2822
+ parentType: fieldBaseType,
2823
+ subSelection: this.selectionSet,
2824
+ fragments,
2825
+ validator: fieldValidator,
2826
+ // We can never apply a fragments that has directives on it at the field level.
2827
+ canUseFullMatchingFragment: (fragment) => fragment.appliedDirectives.length === 0,
2828
+ });
2783
2829
 
2784
- if (optimized instanceof NamedFragmentDefinition) {
2785
- optimizedSelection = selectionSetOf(fieldBaseType, new FragmentSpreadSelection(fieldBaseType, fragments, optimized, []));
2786
- } else {
2787
- optimizedSelection = optimized;
2788
- }
2830
+ let optimizedSelection;
2831
+ if (optimized instanceof NamedFragmentDefinition) {
2832
+ optimizedSelection = selectionSetOf(fieldBaseType, new FragmentSpreadSelection(fieldBaseType, fragments, optimized, []));
2833
+ } else {
2834
+ optimizedSelection = optimized;
2789
2835
  }
2790
2836
 
2791
2837
  // Then, recurse inside the field sub-selection (note that if we matched some fragments above,
@@ -2822,7 +2868,7 @@ export class FieldSelection extends AbstractSelection<Field<any>, undefined, Fie
2822
2868
  }
2823
2869
 
2824
2870
  /**
2825
- * Returns a field selection "equivalent" to the one represented by this object, but such that its parent type
2871
+ * Returns a field selection "equivalent" to the one represented by this object, but such that its parent type
2826
2872
  * is the one provided as argument.
2827
2873
  *
2828
2874
  * Obviously, this operation will only succeed if this selection (both the field itself and its subselections)
@@ -2988,7 +3034,7 @@ export abstract class FragmentSelection extends AbstractSelection<FragmentElemen
2988
3034
  );
2989
3035
  }
2990
3036
  }
2991
-
3037
+
2992
3038
  filterRecursiveDepthFirst(predicate: (selection: Selection) => boolean): FragmentSelection | undefined {
2993
3039
  // Note that we essentially expand all fragments as part of this.
2994
3040
  const updatedSelectionSet = this.selectionSet.filterRecursiveDepthFirst(predicate);
@@ -2998,7 +3044,7 @@ export abstract class FragmentSelection extends AbstractSelection<FragmentElemen
2998
3044
 
2999
3045
  return predicate(thisWithFilteredSelectionSet) ? thisWithFilteredSelectionSet : undefined;
3000
3046
  }
3001
-
3047
+
3002
3048
  hasDefer(): boolean {
3003
3049
  return this.element.hasDefer() || this.selectionSet.hasDefer();
3004
3050
  }
@@ -3006,6 +3052,26 @@ export abstract class FragmentSelection extends AbstractSelection<FragmentElemen
3006
3052
  abstract equals(that: Selection): boolean;
3007
3053
 
3008
3054
  abstract contains(that: Selection): ContainsResult;
3055
+
3056
+ normalize({ parentType, recursive }: { parentType: CompositeType, recursive? : boolean }): FragmentSelection | SelectionSet | undefined {
3057
+ const thisCondition = this.element.typeCondition;
3058
+
3059
+ // This method assumes by contract that `parentType` runtimes intersects `this.parentType`'s, but `parentType`
3060
+ // runtimes may be a subset. So first check if the selection should not be discarded on that account (that
3061
+ // is, we should not keep the selection if its condition runtimes don't intersect at all with those of
3062
+ // `parentType` as that would ultimately make an invalid selection set).
3063
+ if (thisCondition && parentType !== this.parentType) {
3064
+ const conditionRuntimes = possibleRuntimeTypes(thisCondition);
3065
+ const typeRuntimes = possibleRuntimeTypes(parentType);
3066
+ if (!conditionRuntimes.some((t) => typeRuntimes.includes(t))) {
3067
+ return undefined;
3068
+ }
3069
+ }
3070
+
3071
+ return this.normalizeKnowingItIntersects({ parentType, recursive });
3072
+ }
3073
+
3074
+ protected abstract normalizeKnowingItIntersects({ parentType, recursive }: { parentType: CompositeType, recursive? : boolean }): FragmentSelection | SelectionSet | undefined;
3009
3075
  }
3010
3076
 
3011
3077
  class InlineFragmentSelection extends FragmentSelection {
@@ -3090,7 +3156,7 @@ class InlineFragmentSelection extends FragmentSelection {
3090
3156
  };
3091
3157
  }
3092
3158
 
3093
- optimize(fragments: NamedFragments, validator: FieldsConflictValidator): FragmentSelection {
3159
+ optimize(fragments: NamedFragments, validator: FieldsConflictMultiBranchValidator): FragmentSelection {
3094
3160
  let optimizedSelection = this.selectionSet;
3095
3161
 
3096
3162
  // First, see if we can reuse fragments for the selection of this field.
@@ -3173,21 +3239,9 @@ class InlineFragmentSelection extends FragmentSelection {
3173
3239
  : this.withUpdatedComponents(newElement, newSelection);
3174
3240
  }
3175
3241
 
3176
- normalize({ parentType, recursive }: { parentType: CompositeType, recursive? : boolean }): FragmentSelection | SelectionSet | undefined {
3242
+ protected normalizeKnowingItIntersects({ parentType, recursive }: { parentType: CompositeType, recursive? : boolean }): FragmentSelection | SelectionSet | undefined {
3177
3243
  const thisCondition = this.element.typeCondition;
3178
3244
 
3179
- // This method assumes by contract that `parentType` runtimes intersects `this.parentType`'s, but `parentType`
3180
- // runtimes may be a subset. So first check if the selection should not be discarded on that account (that
3181
- // is, we should not keep the selection if its condition runtimes don't intersect at all with those of
3182
- // `parentType` as that would ultimately make an invalid selection set).
3183
- if (thisCondition && parentType !== this.parentType) {
3184
- const conditionRuntimes = possibleRuntimeTypes(thisCondition);
3185
- const typeRuntimes = possibleRuntimeTypes(parentType);
3186
- if (!conditionRuntimes.some((t) => typeRuntimes.includes(t))) {
3187
- return undefined;
3188
- }
3189
- }
3190
-
3191
3245
  // We know the condition is "valid", but it may not be useful. That said, if the condition has directives,
3192
3246
  // we preserve the fragment no matter what.
3193
3247
  if (this.element.appliedDirectives.length === 0) {
@@ -3327,7 +3381,7 @@ class FragmentSpreadSelection extends FragmentSelection {
3327
3381
  assert(false, `Unsupported`);
3328
3382
  }
3329
3383
 
3330
- normalize({ parentType }: { parentType: CompositeType }): FragmentSelection {
3384
+ normalizeKnowingItIntersects({ parentType }: { parentType: CompositeType }): FragmentSelection {
3331
3385
  // We must update the spread parent type if necessary since we're not going deeper,
3332
3386
  // or we'll be fundamentally losing context.
3333
3387
  assert(parentType.schema() === this.parentType.schema(), 'Should not try to normalize using a type from another schema');
@@ -3363,7 +3417,7 @@ class FragmentSpreadSelection extends FragmentSelection {
3363
3417
  };
3364
3418
  }
3365
3419
 
3366
- optimize(_1: NamedFragments, _2: FieldsConflictValidator): FragmentSelection {
3420
+ optimize(_1: NamedFragments, _2: FieldsConflictMultiBranchValidator): FragmentSelection {
3367
3421
  return this;
3368
3422
  }
3369
3423