@apollo/gateway 0.48.1 → 0.50.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 (46) hide show
  1. package/README.md +7 -5
  2. package/dist/__generated__/graphqlTypes.d.ts +4 -0
  3. package/dist/__generated__/graphqlTypes.d.ts.map +1 -1
  4. package/dist/__generated__/graphqlTypes.js.map +1 -1
  5. package/dist/config.d.ts +6 -2
  6. package/dist/config.d.ts.map +1 -1
  7. package/dist/config.js +1 -0
  8. package/dist/config.js.map +1 -1
  9. package/dist/executeQueryPlan.d.ts.map +1 -1
  10. package/dist/executeQueryPlan.js +4 -3
  11. package/dist/executeQueryPlan.js.map +1 -1
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js +19 -8
  14. package/dist/index.js.map +1 -1
  15. package/dist/schema-helper/index.js +5 -1
  16. package/dist/schema-helper/index.js.map +1 -1
  17. package/dist/supergraphManagers/IntrospectAndCompose/index.js.map +1 -1
  18. package/dist/supergraphManagers/LegacyFetcher/index.js.map +1 -1
  19. package/dist/supergraphManagers/LocalCompose/index.js.map +1 -1
  20. package/dist/supergraphManagers/UplinkFetcher/index.d.ts +3 -1
  21. package/dist/supergraphManagers/UplinkFetcher/index.d.ts.map +1 -1
  22. package/dist/supergraphManagers/UplinkFetcher/index.js +20 -4
  23. package/dist/supergraphManagers/UplinkFetcher/index.js.map +1 -1
  24. package/dist/supergraphManagers/UplinkFetcher/loadSupergraphSdlFromStorage.d.ts +3 -2
  25. package/dist/supergraphManagers/UplinkFetcher/loadSupergraphSdlFromStorage.d.ts.map +1 -1
  26. package/dist/supergraphManagers/UplinkFetcher/loadSupergraphSdlFromStorage.js +9 -3
  27. package/dist/supergraphManagers/UplinkFetcher/loadSupergraphSdlFromStorage.js.map +1 -1
  28. package/package.json +6 -6
  29. package/src/__generated__/graphqlTypes.ts +11 -2
  30. package/src/__tests__/CucumberREADME.md +1 -0
  31. package/src/__tests__/build-query-plan-fragmentization.feature +10 -0
  32. package/src/__tests__/build-query-plan.feature +84 -16
  33. package/src/__tests__/buildQueryPlan.test.ts +272 -1
  34. package/src/__tests__/gateway/lifecycle-hooks.test.ts +3 -3
  35. package/src/__tests__/gateway/reporting.test.ts +5 -0
  36. package/src/__tests__/gateway/supergraphSdl.test.ts +3 -3
  37. package/src/__tests__/integration/abstract-types.test.ts +3 -3
  38. package/src/__tests__/integration/configuration.test.ts +0 -11
  39. package/src/__tests__/integration/requires.test.ts +1 -1
  40. package/src/__tests__/integration/value-types.test.ts +1 -1
  41. package/src/config.ts +11 -6
  42. package/src/executeQueryPlan.ts +4 -0
  43. package/src/index.ts +12 -6
  44. package/src/supergraphManagers/UplinkFetcher/__tests__/loadSupergraphSdlFromStorage.test.ts +41 -0
  45. package/src/supergraphManagers/UplinkFetcher/index.ts +37 -18
  46. package/src/supergraphManagers/UplinkFetcher/loadSupergraphSdlFromStorage.ts +9 -1
@@ -7,7 +7,12 @@ import {
7
7
  fixturesWithUpdate,
8
8
  } from 'apollo-federation-integration-testsuite';
9
9
  import { getFederatedTestingSchema } from './execution-utils';
10
- import { QueryPlanner, FetchNode } from '@apollo/query-planner';
10
+ import {
11
+ QueryPlanner,
12
+ FetchNode,
13
+ SequenceNode,
14
+ FlattenNode,
15
+ } from '@apollo/query-planner';
11
16
 
12
17
  expect.addSnapshotSerializer(astSerializer);
13
18
  expect.addSnapshotSerializer(queryPlanSerializer);
@@ -1935,4 +1940,270 @@ describe('buildQueryPlan', () => {
1935
1940
  );
1936
1941
  });
1937
1942
  });
1943
+
1944
+ it(`when key is marked external inside another type`, () => {
1945
+ const operationString = `#graphql
1946
+ query {
1947
+ libraryAccount {
1948
+ description
1949
+ library {
1950
+ id
1951
+ name
1952
+ }
1953
+ }
1954
+ }
1955
+ `;
1956
+
1957
+ const operationDocument = gql(operationString);
1958
+
1959
+ const queryPlan = queryPlanner.buildQueryPlan(
1960
+ buildOperationContext({
1961
+ schema,
1962
+ operationDocument,
1963
+ }),
1964
+ );
1965
+
1966
+ expect(queryPlan).toMatchInlineSnapshot(`
1967
+ QueryPlan {
1968
+ Fetch(service: "accounts") {
1969
+ {
1970
+ libraryAccount {
1971
+ description
1972
+ library {
1973
+ id
1974
+ name
1975
+ }
1976
+ }
1977
+ }
1978
+ },
1979
+ }
1980
+ `);
1981
+ });
1982
+
1983
+ describe('fetch operation names', () => {
1984
+ it('handle subgraph with - in the name', () => {
1985
+ const subgraph1 = {
1986
+ name: 'S1',
1987
+ typeDefs: gql`
1988
+ type Query {
1989
+ t: T
1990
+ }
1991
+ type T @key(fields: "id") {
1992
+ id: ID!
1993
+ }
1994
+ `,
1995
+ };
1996
+
1997
+ const subgraph2 = {
1998
+ name: 'non-graphql-name',
1999
+ typeDefs: gql`
2000
+ extend type T @key(fields: "id") {
2001
+ id: ID! @external
2002
+ x: Int
2003
+ }
2004
+ `,
2005
+ };
2006
+
2007
+ ({ schema, queryPlanner } = getFederatedTestingSchema([
2008
+ subgraph1,
2009
+ subgraph2,
2010
+ ]));
2011
+
2012
+ const operationDocument = gql`
2013
+ query myOp {
2014
+ t {
2015
+ x
2016
+ }
2017
+ }
2018
+ `;
2019
+
2020
+ const plan = queryPlanner.buildQueryPlan(
2021
+ buildOperationContext({ schema, operationDocument }),
2022
+ );
2023
+
2024
+ expect(plan).toMatchInlineSnapshot(`
2025
+ QueryPlan {
2026
+ Sequence {
2027
+ Fetch(service: "S1") {
2028
+ {
2029
+ t {
2030
+ __typename
2031
+ id
2032
+ }
2033
+ }
2034
+ },
2035
+ Flatten(path: "t") {
2036
+ Fetch(service: "non-graphql-name") {
2037
+ {
2038
+ ... on T {
2039
+ __typename
2040
+ id
2041
+ }
2042
+ } =>
2043
+ {
2044
+ ... on T {
2045
+ x
2046
+ }
2047
+ }
2048
+ },
2049
+ },
2050
+ },
2051
+ }
2052
+ `);
2053
+ const fetch = ((plan.node as SequenceNode).nodes[1] as FlattenNode)
2054
+ .node as FetchNode;
2055
+ expect(fetch.operation).toMatch(/^query myOp__non_graphql_name__1.*/i);
2056
+ });
2057
+
2058
+ it('ensures sanitization applies repeatedly', () => {
2059
+ const subgraph1 = {
2060
+ name: 'S1',
2061
+ typeDefs: gql`
2062
+ type Query {
2063
+ t: T
2064
+ }
2065
+ type T @key(fields: "id") {
2066
+ id: ID!
2067
+ }
2068
+ `,
2069
+ };
2070
+
2071
+ const subgraph2 = {
2072
+ name: 'a-na&me-with-plen&ty-replace*ments',
2073
+ typeDefs: gql`
2074
+ extend type T @key(fields: "id") {
2075
+ id: ID! @external
2076
+ x: Int
2077
+ }
2078
+ `,
2079
+ };
2080
+
2081
+ ({ schema, queryPlanner } = getFederatedTestingSchema([
2082
+ subgraph1,
2083
+ subgraph2,
2084
+ ]));
2085
+
2086
+ const operationDocument = gql`
2087
+ query myOp {
2088
+ t {
2089
+ x
2090
+ }
2091
+ }
2092
+ `;
2093
+
2094
+ const plan = queryPlanner.buildQueryPlan(
2095
+ buildOperationContext({ schema, operationDocument }),
2096
+ );
2097
+
2098
+ expect(plan).toMatchInlineSnapshot(`
2099
+ QueryPlan {
2100
+ Sequence {
2101
+ Fetch(service: "S1") {
2102
+ {
2103
+ t {
2104
+ __typename
2105
+ id
2106
+ }
2107
+ }
2108
+ },
2109
+ Flatten(path: "t") {
2110
+ Fetch(service: "a-na&me-with-plen&ty-replace*ments") {
2111
+ {
2112
+ ... on T {
2113
+ __typename
2114
+ id
2115
+ }
2116
+ } =>
2117
+ {
2118
+ ... on T {
2119
+ x
2120
+ }
2121
+ }
2122
+ },
2123
+ },
2124
+ },
2125
+ }
2126
+ `);
2127
+
2128
+ const fetch = ((plan.node as SequenceNode).nodes[1] as FlattenNode)
2129
+ .node as FetchNode;
2130
+ expect(fetch.operation).toMatch(
2131
+ /^query myOp__a_name_with_plenty_replacements__1.*/i,
2132
+ );
2133
+ });
2134
+
2135
+ it('handle very non-graph subgraph name', () => {
2136
+ const subgraph1 = {
2137
+ name: 'S1',
2138
+ typeDefs: gql`
2139
+ type Query {
2140
+ t: T
2141
+ }
2142
+ type T @key(fields: "id") {
2143
+ id: ID!
2144
+ }
2145
+ `,
2146
+ };
2147
+
2148
+ const subgraph2 = {
2149
+ name: '42!',
2150
+ typeDefs: gql`
2151
+ extend type T @key(fields: "id") {
2152
+ id: ID! @external
2153
+ x: Int
2154
+ }
2155
+ `,
2156
+ };
2157
+
2158
+ ({ schema, queryPlanner } = getFederatedTestingSchema([
2159
+ subgraph1,
2160
+ subgraph2,
2161
+ ]));
2162
+
2163
+ const operationDocument = gql`
2164
+ query myOp {
2165
+ t {
2166
+ x
2167
+ }
2168
+ }
2169
+ `;
2170
+
2171
+ const plan = queryPlanner.buildQueryPlan(
2172
+ buildOperationContext({ schema, operationDocument }),
2173
+ );
2174
+ expect(plan).toMatchInlineSnapshot(`
2175
+ QueryPlan {
2176
+ Sequence {
2177
+ Fetch(service: "S1") {
2178
+ {
2179
+ t {
2180
+ __typename
2181
+ id
2182
+ }
2183
+ }
2184
+ },
2185
+ Flatten(path: "t") {
2186
+ Fetch(service: "42!") {
2187
+ {
2188
+ ... on T {
2189
+ __typename
2190
+ id
2191
+ }
2192
+ } =>
2193
+ {
2194
+ ... on T {
2195
+ x
2196
+ }
2197
+ }
2198
+ },
2199
+ },
2200
+ },
2201
+ }
2202
+ `);
2203
+ const fetch = ((plan.node as SequenceNode).nodes[1] as FlattenNode)
2204
+ .node as FetchNode;
2205
+
2206
+ expect(fetch.operation).toMatch(/^query myOp___42__1.*/i);
2207
+ });
2208
+ });
1938
2209
  });
@@ -135,13 +135,13 @@ describe('lifecycle hooks', () => {
135
135
 
136
136
  const [firstCall, secondCall] = mockDidUpdate.mock.calls;
137
137
 
138
- const expectedFirstId = '562c22b3382b56b1651944a96e89a361fe847b9b32660eae5ecbd12adc20bf8b'
138
+ const expectedFirstId = '1cb734eadae95dd1778f3fe3c9df6cbaecb95e80c2bcefed397c63ec72469032'
139
139
  expect(firstCall[0]!.compositionId).toEqual(expectedFirstId);
140
140
  // first call should have no second "previous" argument
141
141
  expect(firstCall[1]).toBeUndefined();
142
142
 
143
143
  expect(secondCall[0]!.compositionId).toEqual(
144
- '0ced02894592ade4376276d11735b46723eb84850c32765cb78502ba5c29a563',
144
+ '5d24c0f42d0d372c2ad671567cee186e3680400dffbcc3430cb57b9dc04b2be2',
145
145
  );
146
146
  // second call should have previous info in the second arg
147
147
  expect(secondCall[1]!.compositionId).toEqual(expectedFirstId);
@@ -177,7 +177,7 @@ describe('lifecycle hooks', () => {
177
177
  );
178
178
  });
179
179
 
180
- it('registers schema change callbacks when experimental_pollInterval is set for unmanaged configs', async () => {
180
+ it('registers schema change callbacks when pollIntervalInMs is set for unmanaged configs', async () => {
181
181
  const experimental_updateServiceDefinitions: Experimental_UpdateServiceDefinitions =
182
182
  jest.fn(async (_config) => {
183
183
  return { serviceDefinitions, isNewSchema: true };
@@ -228,6 +228,7 @@ describe('reporting', () => {
228
228
  "seconds": "1562203363",
229
229
  },
230
230
  "header": "<HEADER>",
231
+ "operationCount": 1,
231
232
  "tracesPerQuery": Object {
232
233
  "# -
233
234
  {me{name{first last}}topProducts{name}}": Object {
@@ -299,6 +300,7 @@ describe('reporting', () => {
299
300
  "nanos": 123000000,
300
301
  "seconds": "1562203363",
301
302
  },
303
+ "fieldExecutionWeight": 1,
302
304
  "root": Object {
303
305
  "child": Array [
304
306
  Object {
@@ -364,6 +366,7 @@ describe('reporting', () => {
364
366
  "nanos": 123000000,
365
367
  "seconds": "1562203363",
366
368
  },
369
+ "fieldExecutionWeight": 1,
367
370
  "root": Object {
368
371
  "child": Array [
369
372
  Object {
@@ -465,6 +468,7 @@ describe('reporting', () => {
465
468
  "nanos": 123000000,
466
469
  "seconds": "1562203363",
467
470
  },
471
+ "fieldExecutionWeight": 1,
468
472
  "root": Object {
469
473
  "child": Array [
470
474
  Object {
@@ -568,6 +572,7 @@ describe('reporting', () => {
568
572
  "nanos": 123000000,
569
573
  "seconds": "1562203363",
570
574
  },
575
+ "fieldExecutionWeight": 1,
571
576
  "root": Object {
572
577
  "child": Array [
573
578
  Object {
@@ -184,7 +184,7 @@ describe('Using supergraphSdl dynamic configuration', () => {
184
184
  const { state, compositionId } = gateway.__testing();
185
185
  expect(state.phase).toEqual('loaded');
186
186
  expect(compositionId).toEqual(
187
- '562c22b3382b56b1651944a96e89a361fe847b9b32660eae5ecbd12adc20bf8b',
187
+ '1cb734eadae95dd1778f3fe3c9df6cbaecb95e80c2bcefed397c63ec72469032',
188
188
  );
189
189
 
190
190
  await gateway.stop();
@@ -209,7 +209,7 @@ describe('Using supergraphSdl dynamic configuration', () => {
209
209
  const { state, compositionId } = gateway.__testing();
210
210
  expect(state.phase).toEqual('loaded');
211
211
  expect(compositionId).toEqual(
212
- '562c22b3382b56b1651944a96e89a361fe847b9b32660eae5ecbd12adc20bf8b',
212
+ '1cb734eadae95dd1778f3fe3c9df6cbaecb95e80c2bcefed397c63ec72469032',
213
213
  );
214
214
 
215
215
  await expect(healthCheckCallback!(supergraphSdl)).resolves.toBeUndefined();
@@ -291,7 +291,7 @@ describe('Using supergraphSdl dynamic configuration', () => {
291
291
  const { state, compositionId } = gateway.__testing();
292
292
  expect(state.phase).toEqual('loaded');
293
293
  expect(compositionId).toEqual(
294
- '562c22b3382b56b1651944a96e89a361fe847b9b32660eae5ecbd12adc20bf8b',
294
+ '1cb734eadae95dd1778f3fe3c9df6cbaecb95e80c2bcefed397c63ec72469032',
295
295
  );
296
296
 
297
297
  await expect(healthCheckCallback!(supergraphSdl)).rejects.toThrowError(
@@ -297,7 +297,7 @@ it('prunes unfilled type conditions', async () => {
297
297
 
298
298
  it('fetches interfaces returned from other services', async () => {
299
299
  const query = `#graphql
300
- query GetUserAndProducts {
300
+ query GetUserAndProductsWithPriceAndTitle {
301
301
  me {
302
302
  reviews {
303
303
  product {
@@ -412,7 +412,7 @@ it('fetches interfaces returned from other services', async () => {
412
412
 
413
413
  it('fetches composite fields from a foreign type casted to an interface [@provides field]', async () => {
414
414
  const query = `#graphql
415
- query GetUserAndProducts {
415
+ query GetUserAndProductsWithPriceAndName {
416
416
  me {
417
417
  reviews {
418
418
  product {
@@ -624,7 +624,7 @@ it('allows for extending an interface from another service with fields', async (
624
624
  describe('unions', () => {
625
625
  it('handles unions from the same service', async () => {
626
626
  const query = `#graphql
627
- query GetUserAndProducts {
627
+ query GetUserAndProductsWithBrandInfo {
628
628
  me {
629
629
  reviews {
630
630
  product {
@@ -443,15 +443,4 @@ describe('deprecation warnings', () => {
443
443
  'The `schemaConfigDeliveryEndpoint` option is deprecated and will be removed in a future version of `@apollo/gateway`. Please migrate to the equivalent (array form) `uplinkEndpoints` configuration option.',
444
444
  );
445
445
  });
446
-
447
- it('warns with `experimental_pollInterval` option set', async () => {
448
- new ApolloGateway({
449
- experimental_pollInterval: 10000,
450
- logger,
451
- });
452
-
453
- expect(logger.warn).toHaveBeenCalledWith(
454
- 'The `experimental_pollInterval` option is deprecated and will be removed in a future version of `@apollo/gateway`. Please migrate to the equivalent `pollIntervalInMs` configuration option.',
455
- );
456
- });
457
446
  });
@@ -6,7 +6,7 @@ expect.addSnapshotSerializer(astSerializer);
6
6
  expect.addSnapshotSerializer(queryPlanSerializer);
7
7
  it('supports passing additional fields defined by a requires', async () => {
8
8
  const query = `#graphql
9
- query GetReviwedBookNames {
9
+ query GetReviewedBookNames {
10
10
  me {
11
11
  reviews {
12
12
  product {
@@ -22,7 +22,7 @@ describe('value types', () => {
22
22
  }
23
23
  }
24
24
 
25
- query ProducsWithMetadata {
25
+ query ProductsWithMetadata {
26
26
  topProducts(first: 10) {
27
27
  upc
28
28
  ... on Book {
package/src/config.ts CHANGED
@@ -81,6 +81,7 @@ export interface ServiceDefinitionUpdate {
81
81
  export interface SupergraphSdlUpdate {
82
82
  id: string;
83
83
  supergraphSdl: string;
84
+ minDelaySeconds?: number;
84
85
  }
85
86
 
86
87
  export function isSupergraphSdlUpdate(
@@ -125,11 +126,6 @@ interface GatewayConfigBase {
125
126
  // experimental observability callbacks
126
127
  experimental_didResolveQueryPlan?: Experimental_DidResolveQueryPlanCallback;
127
128
  experimental_didUpdateSupergraph?: Experimental_DidUpdateSupergraphCallback;
128
- /**
129
- * @deprecated use `pollIntervalInMs` instead
130
- */
131
- experimental_pollInterval?: number;
132
- pollIntervalInMs?: number;
133
129
  experimental_approximateQueryPlanStoreMiB?: number;
134
130
  experimental_autoFragmentization?: boolean;
135
131
  fetcher?: typeof fetch;
@@ -150,6 +146,7 @@ export interface ServiceListGatewayConfig extends GatewayConfigBase {
150
146
  | ((
151
147
  service: ServiceEndpointDefinition,
152
148
  ) => Promise<HeadersInit> | HeadersInit);
149
+ pollIntervalInMs?: number;
153
150
  }
154
151
 
155
152
  export interface ManagedGatewayConfig extends GatewayConfigBase {
@@ -168,6 +165,11 @@ export interface ManagedGatewayConfig extends GatewayConfigBase {
168
165
  */
169
166
  uplinkEndpoints?: string[];
170
167
  uplinkMaxRetries?: number;
168
+ /**
169
+ * @deprecated use `fallbackPollIntervalInMs` instead
170
+ */
171
+ pollIntervalInMs?: number;
172
+ fallbackPollIntervalInMs?: number;
171
173
  }
172
174
 
173
175
  // TODO(trevor:removeServiceList): migrate users to `supergraphSdl` function option
@@ -176,6 +178,7 @@ interface ManuallyManagedServiceDefsGatewayConfig extends GatewayConfigBase {
176
178
  * @deprecated: use `supergraphSdl` instead (either as a `SupergraphSdlHook` or `SupergraphManager`)
177
179
  */
178
180
  experimental_updateServiceDefinitions: Experimental_UpdateServiceDefinitions;
181
+ pollIntervalInMs?: number;
179
182
  }
180
183
 
181
184
  // TODO(trevor:removeServiceList): migrate users to `supergraphSdl` function option
@@ -185,6 +188,7 @@ interface ExperimentalManuallyManagedSupergraphSdlGatewayConfig
185
188
  * @deprecated: use `supergraphSdl` instead (either as a `SupergraphSdlHook` or `SupergraphManager`)
186
189
  */
187
190
  experimental_updateSupergraphSdl: Experimental_UpdateSupergraphSdl;
191
+ pollIntervalInMs?: number;
188
192
  }
189
193
 
190
194
  export function isManuallyManagedSupergraphSdlGatewayConfig(
@@ -238,7 +242,7 @@ type ManuallyManagedGatewayConfig =
238
242
  | ManuallyManagedServiceDefsGatewayConfig
239
243
  | ExperimentalManuallyManagedSupergraphSdlGatewayConfig
240
244
  | ManuallyManagedSupergraphSdlGatewayConfig
241
- // TODO(trevor:removeServiceList
245
+ // TODO(trevor:removeServiceList)
242
246
  | ServiceListGatewayConfig;
243
247
 
244
248
  // TODO(trevor:removeServiceList)
@@ -322,6 +326,7 @@ export function isManagedConfig(
322
326
  return (
323
327
  'schemaConfigDeliveryEndpoint' in config ||
324
328
  'uplinkEndpoints' in config ||
329
+ 'fallbackPollIntervalInMs' in config ||
325
330
  (!isLocalConfig(config) &&
326
331
  !isStaticSupergraphSdlConfig(config) &&
327
332
  !isManuallyManagedConfig(config))
@@ -329,6 +329,7 @@ async function executeFetch<TContext>(
329
329
  context,
330
330
  fetch.operation,
331
331
  variables,
332
+ fetch.operationName,
332
333
  );
333
334
 
334
335
  for (const entity of entities) {
@@ -364,6 +365,7 @@ async function executeFetch<TContext>(
364
365
  context,
365
366
  fetch.operation,
366
367
  {...variables, representations},
368
+ fetch.operationName,
367
369
  );
368
370
 
369
371
  if (!dataReceivedFromService) {
@@ -405,6 +407,7 @@ async function executeFetch<TContext>(
405
407
  context: ExecutionContext<TContext>,
406
408
  source: string,
407
409
  variables: Record<string, any>,
410
+ operationName: string | undefined,
408
411
  ): Promise<ResultMap | void | null> {
409
412
  // We declare this as 'any' because it is missing url and method, which
410
413
  // GraphQLRequest.http is supposed to have if it exists.
@@ -433,6 +436,7 @@ async function executeFetch<TContext>(
433
436
  request: {
434
437
  query: source,
435
438
  variables,
439
+ operationName,
436
440
  http,
437
441
  },
438
442
  incomingRequestContext: context.requestContext,
package/src/index.ts CHANGED
@@ -202,8 +202,12 @@ export class ApolloGateway implements GraphQLService {
202
202
  this.experimental_didUpdateSupergraph =
203
203
  config?.experimental_didUpdateSupergraph;
204
204
 
205
- this.pollIntervalInMs =
206
- config?.pollIntervalInMs ?? config?.experimental_pollInterval;
205
+ if (isManagedConfig(this.config)) {
206
+ this.pollIntervalInMs =
207
+ this.config.fallbackPollIntervalInMs ?? this.config.pollIntervalInMs;
208
+ } else if (isServiceListConfig(this.config)) {
209
+ this.pollIntervalInMs = this.config?.pollIntervalInMs;
210
+ }
207
211
 
208
212
  this.issueConfigurationWarningsIfApplicable();
209
213
 
@@ -254,7 +258,7 @@ export class ApolloGateway implements GraphQLService {
254
258
  'Polling Apollo services at a frequency of less than once per 10 ' +
255
259
  'seconds (10000) is disallowed. Instead, the minimum allowed ' +
256
260
  'pollInterval of 10000 will be used. Please reconfigure your ' +
257
- '`pollIntervalInMs` accordingly. If this is problematic for ' +
261
+ '`fallbackPollIntervalInMs` accordingly. If this is problematic for ' +
258
262
  'your team, please contact support.',
259
263
  );
260
264
  }
@@ -288,9 +292,11 @@ export class ApolloGateway implements GraphQLService {
288
292
  );
289
293
  }
290
294
 
291
- if ('experimental_pollInterval' in this.config) {
295
+ if (isManagedConfig(this.config) && 'pollIntervalInMs' in this.config) {
292
296
  this.logger.warn(
293
- 'The `experimental_pollInterval` option is deprecated and will be removed in a future version of `@apollo/gateway`. Please migrate to the equivalent `pollIntervalInMs` configuration option.',
297
+ 'The `pollIntervalInMs` option is deprecated and will be removed in a future version of `@apollo/gateway`. ' +
298
+ 'Please migrate to the equivalent `fallbackPollIntervalInMs` configuration option. ' +
299
+ 'The poll interval is now defined by Uplink, this option will only be used if it is greater than the value defined by Uplink or as a fallback.',
294
300
  );
295
301
  }
296
302
  }
@@ -404,7 +410,7 @@ export class ApolloGateway implements GraphQLService {
404
410
  subgraphHealthCheck: this.config.serviceHealthCheck,
405
411
  fetcher: this.fetcher,
406
412
  logger: this.logger,
407
- pollIntervalInMs: this.pollIntervalInMs ?? 10000,
413
+ fallbackPollIntervalInMs: this.pollIntervalInMs ?? 10000,
408
414
  }),
409
415
  );
410
416
  }
@@ -65,6 +65,7 @@ describe('loadSupergraphSdlFromStorage', () => {
65
65
  compositionId: "originalId-1234",
66
66
  maxRetries: 1,
67
67
  roundRobinSeed: 0,
68
+ earliestFetchTime: null,
68
69
  });
69
70
 
70
71
  expect(result).toMatchObject({
@@ -88,6 +89,7 @@ describe('loadSupergraphSdlFromStorage', () => {
88
89
  compositionId: "originalId-1234",
89
90
  maxRetries: 1,
90
91
  roundRobinSeed: 0,
92
+ earliestFetchTime: null,
91
93
  }),
92
94
  ).rejects.toThrowError(
93
95
  new UplinkFetcherError(
@@ -388,10 +390,49 @@ describe("loadSupergraphSdlFromUplinks", () => {
388
390
  compositionId: "id-1234",
389
391
  maxRetries: 5,
390
392
  roundRobinSeed: 0,
393
+ earliestFetchTime: null,
391
394
  });
392
395
 
393
396
  expect(result).toBeNull();
394
397
  expect(fetcher).toHaveBeenCalledTimes(1);
395
398
  });
399
+
400
+ it("Waits the correct time before retrying", async () => {
401
+ const timeoutSpy = jest.spyOn(global, 'setTimeout');
402
+
403
+ mockSupergraphSdlRequest('originalId-1234', mockCloudConfigUrl1).reply(500);
404
+ mockSupergraphSdlRequestIfAfter('originalId-1234', mockCloudConfigUrl2).reply(
405
+ 200,
406
+ JSON.stringify({
407
+ data: {
408
+ routerConfig: {
409
+ __typename: 'RouterConfigResult',
410
+ id: 'originalId-1234',
411
+ supergraphSdl: getTestingSupergraphSdl()
412
+ },
413
+ },
414
+ }),
415
+ );
416
+ const fetcher = getDefaultFetcher();
417
+
418
+ await loadSupergraphSdlFromUplinks({
419
+ graphRef,
420
+ apiKey,
421
+ endpoints: [mockCloudConfigUrl1, mockCloudConfigUrl2],
422
+ errorReportingEndpoint: undefined,
423
+ fetcher: fetcher,
424
+ compositionId: "originalId-1234",
425
+ maxRetries: 1,
426
+ roundRobinSeed: 0,
427
+ earliestFetchTime: new Date(Date.now() + 1000),
428
+ });
429
+
430
+ // test if setTimeout was called with a value in range to deal with time jitter
431
+ const setTimeoutCall = timeoutSpy.mock.calls[1][1];
432
+ expect(setTimeoutCall).toBeLessThanOrEqual(1000);
433
+ expect(setTimeoutCall).toBeGreaterThanOrEqual(900);
434
+
435
+ timeoutSpy.mockRestore();
436
+ });
396
437
  });
397
438