diff --git a/CHANGELOG.md b/CHANGELOG.md index 27244d6513..c1f8424d7c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -611,6 +611,12 @@ Our versioning strategy is as follows: * `import { editingDataService } from '@sitecore-jss/sitecore-jss-nextjs/editing';` * `import { EditingRenderMiddleware } from '@sitecore-jss/sitecore-jss-nextjs/editing';` +## 20.3.0 + +### 🎉 New Features & Improvements + +* `[sitecore-jss]` Retry policy to handle transient network errors. Users can pass `retryStrategy` to configure custom retry config to the services. They can customize the error codes and the number of retries. It consist of two functions shouldRetry and getDelay. ([#1731](https://github.com/Sitecore/jss/pull/1731)) + ## 20.2.3 ### 🐛 Bug Fixes diff --git a/packages/create-sitecore-jss/src/templates/nextjs/src/lib/dictionary-service-factory.ts b/packages/create-sitecore-jss/src/templates/nextjs/src/lib/dictionary-service-factory.ts index 963f814194..3c0fd28bc5 100644 --- a/packages/create-sitecore-jss/src/templates/nextjs/src/lib/dictionary-service-factory.ts +++ b/packages/create-sitecore-jss/src/templates/nextjs/src/lib/dictionary-service-factory.ts @@ -28,11 +28,14 @@ export class DictionaryServiceFactory { rootItemId: '{GUID}' */ /* - GraphQL endpoint may reach its rate limit with the amount of Layout and Dictionary requests it receives and throw a rate limit error. + GraphQL endpoint may reach its rate limit with the amount of requests it receives and throw a rate limit error. GraphQL Dictionary and Layout Services can handle rate limit errors from server and attempt a retry on requests. - For this, specify the number of retries the GraphQL client will attempt. + For this, specify the number of 'retries' the GraphQL client will attempt. It will only try the request once by default. - retries: 'number' + + Additionally, you have the flexibility to customize the retry strategy by passing a 'retryStrategy'. + By default it uses the `DefaultRetryStrategy` with exponential back-off factor of 2 for error codes 429, + 502, 503, 504, 520, 521, 522, 523, and 524. You can use this class or your own implementation of `RetryStrategy`. */ retries: (process.env.GRAPH_QL_SERVICE_RETRIES && diff --git a/packages/create-sitecore-jss/src/templates/nextjs/src/lib/layout-service-factory.ts b/packages/create-sitecore-jss/src/templates/nextjs/src/lib/layout-service-factory.ts index c175488bd7..1de97adc20 100644 --- a/packages/create-sitecore-jss/src/templates/nextjs/src/lib/layout-service-factory.ts +++ b/packages/create-sitecore-jss/src/templates/nextjs/src/lib/layout-service-factory.ts @@ -21,11 +21,14 @@ export class LayoutServiceFactory { siteName, clientFactory, /* - GraphQL endpoint may reach its rate limit with the amount of Layout and Dictionary requests it receives and throw a rate limit error. + GraphQL endpoint may reach its rate limit with the amount of requests it receives and throw a rate limit error. GraphQL Dictionary and Layout Services can handle rate limit errors from server and attempt a retry on requests. - For this, specify the number of retries the GraphQL client will attempt. + For this, specify the number of 'retries' the GraphQL client will attempt. It will only try the request once by default. - retries: 'number' + + Additionally, you have the flexibility to customize the retry strategy by passing a 'retryStrategy'. + By default it uses the `DefaultRetryStrategy` with exponential back-off factor of 2 for error codes 429, + 502, 503, 504, 520, 521, 522, 523, and 524. You can use this class or your own implementation of `RetryStrategy`. */ retries: (process.env.GRAPH_QL_SERVICE_RETRIES && diff --git a/packages/sitecore-jss-angular/src/public_api.ts b/packages/sitecore-jss-angular/src/public_api.ts index 251d0f7704..6ab405679a 100644 --- a/packages/sitecore-jss-angular/src/public_api.ts +++ b/packages/sitecore-jss-angular/src/public_api.ts @@ -56,6 +56,7 @@ export { ComponentFields, ComponentParams, } from '@sitecore-jss/sitecore-jss/layout'; +export { RetryStrategy, DefaultRetryStrategy } from '@sitecore-jss/sitecore-jss/graphql'; export { constants, HttpDataFetcher, HttpResponse, enableDebug } from '@sitecore-jss/sitecore-jss'; export { isServer, diff --git a/packages/sitecore-jss-nextjs/src/graphql/index.ts b/packages/sitecore-jss-nextjs/src/graphql/index.ts index 157e4bed60..fa83a58dd7 100644 --- a/packages/sitecore-jss-nextjs/src/graphql/index.ts +++ b/packages/sitecore-jss-nextjs/src/graphql/index.ts @@ -1,4 +1,6 @@ export { + RetryStrategy, + DefaultRetryStrategy, GraphQLRequestClient, GraphQLRequestClientFactory, GraphQLRequestClientFactoryConfig, diff --git a/packages/sitecore-jss-react/src/index.ts b/packages/sitecore-jss-react/src/index.ts index f48049b761..309a2532af 100644 --- a/packages/sitecore-jss-react/src/index.ts +++ b/packages/sitecore-jss-react/src/index.ts @@ -44,6 +44,7 @@ export { GraphQLDictionaryService, RestDictionaryService, } from '@sitecore-jss/sitecore-jss/i18n'; +export { RetryStrategy, DefaultRetryStrategy } from '@sitecore-jss/sitecore-jss/graphql'; export { mediaApi } from '@sitecore-jss/sitecore-jss/media'; export { getFEAASLibraryStylesheetLinks } from '@sitecore-jss/sitecore-jss/feaas'; export { ComponentFactory } from './components/sharedTypes'; diff --git a/packages/sitecore-jss-vue/src/index.ts b/packages/sitecore-jss-vue/src/index.ts index d9d9cc3b5b..b5915b0a9a 100644 --- a/packages/sitecore-jss-vue/src/index.ts +++ b/packages/sitecore-jss-vue/src/index.ts @@ -39,6 +39,7 @@ export { GraphQLDictionaryService, RestDictionaryService, } from '@sitecore-jss/sitecore-jss/i18n'; +export { RetryStrategy, DefaultRetryStrategy } from '@sitecore-jss/sitecore-jss/graphql'; export { mediaApi } from '@sitecore-jss/sitecore-jss/media'; export { EditFrame } from './components/EditFrame'; export { Placeholder } from './components/Placeholder'; diff --git a/packages/sitecore-jss/package.json b/packages/sitecore-jss/package.json index b577904d71..4f7ff194e0 100644 --- a/packages/sitecore-jss/package.json +++ b/packages/sitecore-jss/package.json @@ -38,6 +38,7 @@ "@types/memory-cache": "^0.2.1", "@types/mocha": "^9.0.0", "@types/node": "^16.11.6", + "@types/sinon": "^17.0.3", "@types/url-parse": "1.4.8", "chai": "^4.2.0", "chai-spies": "^1.0.0", @@ -47,6 +48,7 @@ "mocha": "^10.2.0", "nock": "^13.0.5", "nyc": "^15.1.0", + "sinon": "^17.0.1", "ts-node": "^8.4.1", "tslib": "^1.10.0", "typescript": "~4.3.5" diff --git a/packages/sitecore-jss/src/graphql-request-client.test.ts b/packages/sitecore-jss/src/graphql-request-client.test.ts index ccb785a0ab..72ba8ed9f9 100644 --- a/packages/sitecore-jss/src/graphql-request-client.test.ts +++ b/packages/sitecore-jss/src/graphql-request-client.test.ts @@ -1,6 +1,7 @@ /* eslint-disable no-unused-expressions */ /* eslint-disable dot-notation */ import { expect, use, spy } from 'chai'; +import sinon from 'sinon'; import spies from 'chai-spies'; import nock from 'nock'; import { GraphQLRequestClient } from './graphql-request-client'; @@ -118,7 +119,6 @@ describe('GraphQLRequestClient', () => { ); } }); - it('should throw error when request is aborted with default timeout value', async () => { nock('http://jssnextweb') .post('/graphql') @@ -136,7 +136,7 @@ describe('GraphQLRequestClient', () => { }); it('should use retry and throw error when retries specified', async function() { - this.timeout(6000); + this.timeout(8000); nock('http://jssnextweb') .post('/graphql') .reply(429) @@ -144,7 +144,10 @@ describe('GraphQLRequestClient', () => { .reply(429) .post('/graphql') .reply(429); - const graphQLClient = new GraphQLRequestClient(endpoint, { retries: 2 }); + + const graphQLClient = new GraphQLRequestClient(endpoint, { + retries: 2, + }); spy.on(graphQLClient['client'], 'request'); await graphQLClient.request('test').catch((error) => { expect(error).to.not.be.undefined; @@ -154,7 +157,7 @@ describe('GraphQLRequestClient', () => { }); it('should use retry and resolve if one of the requests resolves', async function() { - this.timeout(6000); + this.timeout(8000); nock('http://jssnextweb') .post('/graphql') .reply(429) @@ -166,7 +169,7 @@ describe('GraphQLRequestClient', () => { result: 'Hello world...', }, }); - const graphQLClient = new GraphQLRequestClient(endpoint, { retries: 3 }); + const graphQLClient = new GraphQLRequestClient(endpoint, { retries: 2 }); spy.on(graphQLClient['client'], 'request'); const data = await graphQLClient.request('test'); @@ -177,7 +180,7 @@ describe('GraphQLRequestClient', () => { }); it('should use [retry-after] header value when response is 429', async function() { - this.timeout(6000); + this.timeout(7000); nock('http://jssnextweb') .post('/graphql') .reply(429, {}, { 'Retry-After': '2' }); @@ -186,15 +189,17 @@ describe('GraphQLRequestClient', () => { await graphQLClient.request('test').catch(() => { expect(graphQLClient['debug']).to.have.been.called.with( - 'Error: Rate limit reached for GraphQL endpoint. Retrying in %ds. Retries left: %d', - 2, + 'Error: %d. Rate limit reached for GraphQL endpoint. Retrying in %dms (attempt %d).', + 429, + 2000, 1 ); spy.restore(graphQLClient); }); }); - it('should throw error when request is aborted with default timeout value after retry', async () => { + it('should throw error when request is aborted value after retry', async function() { + this.timeout(3000); nock('http://jssnextweb') .post('/graphql') .reply(429) @@ -206,13 +211,18 @@ describe('GraphQLRequestClient', () => { }, }); - const graphQLClient = new GraphQLRequestClient(endpoint, { retries: 2 }); + const graphQLClient = new GraphQLRequestClient(endpoint, { retries: 1, timeout: 50 }); spy.on(graphQLClient['client'], 'request'); - await graphQLClient.request('test').catch((error) => { + try { + await graphQLClient.request('test'); + // If the request does not throw an error, fail the test + expect.fail('Expected request to throw an error'); + } catch (error) { expect(graphQLClient['client'].request).to.be.called.exactly(2); expect(error.name).to.equal('AbortError'); + } finally { spy.restore(graphQLClient); - }); + } }); it('should throw error upon request timeout using provided timeout value', async () => { @@ -245,4 +255,154 @@ describe('GraphQLRequestClient', () => { expect(client['timeout']).to.equal(300); }); }); + + describe('Retrayable status codes', () => { + const retryableStatusCodeThrowError = async (statusCode: number) => { + nock('http://jssnextweb') + .post('/graphql') + .reply(statusCode) + .post('/graphql') + .reply(statusCode) + .post('/graphql') + .reply(statusCode); + + const graphQLClient = new GraphQLRequestClient(endpoint, { + retries: 2, + }); + + spy.on(graphQLClient['client'], 'request'); + + try { + await graphQLClient.request('test'); + } catch (error) { + expect(error).to.not.be.undefined; + expect(graphQLClient['client'].request).to.have.been.called.exactly(3); + spy.restore(graphQLClient); + } + }; + + // Test cases for each retryable status code + for (const statusCode of [429, 502, 503, 504, 520, 521, 522, 523, 524]) { + it(`should retry and throw error for ${statusCode} when retries specified`, async function() { + this.timeout(8000); + await retryableStatusCodeThrowError(statusCode); + }); + } + + const retryableStatusCodeResolve = async (statusCode: number) => { + nock('http://jssnextweb') + .post('/graphql') + .reply(statusCode) + .post('/graphql') + .reply(statusCode) + .post('/graphql') + .reply(200, { + data: { + result: 'Hello world...', + }, + }); + + const graphQLClient = new GraphQLRequestClient(endpoint, { + retries: 3, + }); + + spy.on(graphQLClient['client'], 'request'); + + const data = await graphQLClient.request('test'); + + try { + await graphQLClient.request('test'); + expect(data).to.not.be.null; + } catch (error) { + expect(graphQLClient['client'].request).to.have.been.called.exactly(4); + spy.restore(graphQLClient); + } + }; + + // Test cases for each retryable status code + for (const statusCode of [429, 502, 503, 504, 520, 521, 522, 523, 524]) { + it(`should retry and resolve for ${statusCode} if one of the request resolves`, async function() { + this.timeout(16000); + await retryableStatusCodeResolve(statusCode); + }); + } + + it('should retry based on custom retryStrategy', async function() { + this.timeout(8000); + + nock('http://jssnextweb') + .post('/graphql') + .reply(502, { + data: { + result: 'Hello world...', + }, + }); + + const customRetryStrategy = { + shouldRetry: (_: any, attempt: number) => attempt <= 3, + getDelay: () => 1000, + }; + + const graphQLClient = new GraphQLRequestClient(endpoint, { + retries: 4, + retryStrategy: customRetryStrategy, + }); + + spy.on(graphQLClient['client'], 'request'); + + try { + await graphQLClient.request('test'); + } catch (error) { + expect(error).to.not.be.undefined; + expect(graphQLClient['client'].request).to.be.called.exactly(4); + spy.restore(graphQLClient); + } + }); + + it('should delay before retrying based on exponential backoff', async function() { + this.timeout(32000); + + nock('http://jssnextweb') + .post('/graphql') + .reply(429) + .post('/graphql') + .reply(429) + .post('/graphql') + .reply(429) + .post('/graphql') + .reply(429); + + const graphQLClient = new GraphQLRequestClient(endpoint, { + retries: 4, + }); + + spy.on(graphQLClient['client'], 'request'); + + try { + await graphQLClient.request('test'); + + expect(graphQLClient['client'].request).to.have.been.called.exactly(1); + + const clock = sinon.useFakeTimers(); + clock.tick(1000); + + await graphQLClient.request('test'); + expect(graphQLClient['client'].request).to.have.been.called.exactly(2); + + clock.tick(2000); + + await graphQLClient.request('test'); + expect(graphQLClient['client'].request).to.have.been.called.exactly(3); + + clock.tick(4000); + + await graphQLClient.request('test'); + expect(graphQLClient['client'].request).to.have.been.called.exactly(4); + + clock.restore(); + } catch (error) { + console.log('error'); + } + }); + }); }); diff --git a/packages/sitecore-jss/src/graphql-request-client.ts b/packages/sitecore-jss/src/graphql-request-client.ts index 80da5232cf..4eb38ae792 100644 --- a/packages/sitecore-jss/src/graphql-request-client.ts +++ b/packages/sitecore-jss/src/graphql-request-client.ts @@ -15,6 +15,26 @@ export interface GraphQLClient { */ request(query: string | DocumentNode, variables?: { [key: string]: unknown }): Promise; } +/** + * Defines the strategy for retrying GraphQL requests based on errors and attempts. + */ +export interface RetryStrategy { + /** + * Determines whether a request should be retried based on the given error and attempt count. + * @param error - The error received from the GraphQL request. + * @param attempt - The current attempt number. + * @param retries - The number of retries configured. + * @returns A boolean indicating whether to retry the request. + */ + shouldRetry(error: ClientError, attempt: number, retries: number): boolean; + /** + * Calculates the delay (in milliseconds) before the next retry based on the given error and attempt count. + * @param error - The error received from the GraphQL request. + * @param attempt - The current attempt number. + * @returns The delay in milliseconds before the next retry. + */ + getDelay(error: ClientError, attempt: number): number; +} /** * Minimum configuration options for classes that implement @see GraphQLClient @@ -33,13 +53,18 @@ export type GraphQLRequestClientConfig = { */ fetch?: typeof fetch; /** - * GraphQLClient request timeout + * GraphQLClient request timeout (in milliseconds). */ timeout?: number; /** - * Number of retries for client. Will be used if endpoint responds with 429 (rate limit reached) error + * Number of retries for client. Will use the specified `retryStrategy`. */ retries?: number; + /** + * Retry strategy for the client. Uses `DefaultRetryStrategy` by default with exponential + * back-off factor of 2 for codes 429, 502, 503, 504, 520, 521, 522, 523, 524. + */ + retryStrategy?: RetryStrategy; }; /** @@ -57,6 +82,38 @@ export type GraphQLRequestClientFactory = ( */ export type GraphQLRequestClientFactoryConfig = { endpoint: string; apiKey?: string }; +/** + * Represents a default retry strategy for handling retry attempts in case of specific HTTP status codes. + * This class implements the RetryStrategy interface and provides methods to determine whether a request + * should be retried and calculates the delay before the next retry attempt. + */ +export class DefaultRetryStrategy implements RetryStrategy { + private statusCodes: number[]; + private factor: number; + + /** + * @param {number[]} statusCodes HTTP status codes to trigger retries on + * @param {number} factor Factor by which the delay increases with each retry attempt + */ + constructor(statusCodes?: number[], factor?: number) { + this.statusCodes = statusCodes || [429]; + this.factor = factor || 2; + } + + shouldRetry(error: ClientError, attempt: number, retries: number): boolean { + return retries > 0 && attempt <= retries && this.statusCodes.includes(error.response.status); + } + + getDelay(error: ClientError, attempt: number): number { + const rawHeaders = error.response.headers; + const delaySeconds = rawHeaders?.get('Retry-After') + ? Number.parseInt(rawHeaders?.get('Retry-After'), 10) + : Math.pow(this.factor, attempt); + + return delaySeconds * 1000; + } +} + /** * A GraphQL client for Sitecore APIs that uses the 'graphql-request' library. * https://github.com/prisma-labs/graphql-request @@ -65,9 +122,10 @@ export class GraphQLRequestClient implements GraphQLClient { private client: Client; private headers: Record = {}; private debug: Debugger; - private retries: number; private abortTimeout?: TimeoutPromise; private timeout?: number; + private retries: number; + private retryStrategy: RetryStrategy; /** * Provides ability to execute graphql query using given `endpoint` @@ -87,6 +145,9 @@ export class GraphQLRequestClient implements GraphQLClient { this.timeout = clientConfig.timeout; this.retries = clientConfig.retries || 0; + this.retryStrategy = + clientConfig.retryStrategy || + new DefaultRetryStrategy([429, 502, 503, 504, 520, 521, 522, 523, 524]); this.client = new Client(endpoint, { headers: this.headers, fetch: clientConfig.fetch, @@ -117,7 +178,7 @@ export class GraphQLRequestClient implements GraphQLClient { query: string | DocumentNode, variables?: { [key: string]: unknown } ): Promise { - let retriesLeft = this.retries; + let attempt = 1; const retryer = async (): Promise => { // Note we don't have access to raw request/response with graphql-request @@ -134,28 +195,30 @@ export class GraphQLRequestClient implements GraphQLClient { this.abortTimeout = new TimeoutPromise(this.timeout); fetchWithOptionalTimeout.push(this.abortTimeout.start); } + return Promise.race(fetchWithOptionalTimeout).then( (data: T) => { this.abortTimeout?.clear(); this.debug('response in %dms: %o', Date.now() - startTimestamp, data); return Promise.resolve(data); }, - (error: ClientError) => { + async (error: ClientError) => { this.abortTimeout?.clear(); this.debug('response error: %o', error.response || error.message || error); - if (error.response?.status === 429 && retriesLeft > 0) { - const rawHeaders = (error as ClientError)?.response?.headers; - const delaySeconds = - rawHeaders && rawHeaders.get('Retry-After') - ? Number.parseInt(rawHeaders.get('Retry-After'), 10) - : 1; + const status = error.response?.status; + const shouldRetry = this.retryStrategy.shouldRetry(error, attempt, this.retries); + + if (shouldRetry) { + const delayMs = this.retryStrategy.getDelay(error, attempt); this.debug( - 'Error: Rate limit reached for GraphQL endpoint. Retrying in %ds. Retries left: %d', - delaySeconds, - retriesLeft + 'Error: %d. Rate limit reached for GraphQL endpoint. Retrying in %dms (attempt %d).', + status, + delayMs, + attempt ); - retriesLeft--; - return new Promise((resolve) => setTimeout(resolve, delaySeconds * 1000)).then(retryer); + + attempt++; + return new Promise((resolve) => setTimeout(resolve, delayMs)).then(retryer); } else { return Promise.reject(error); } diff --git a/packages/sitecore-jss/src/graphql/index.ts b/packages/sitecore-jss/src/graphql/index.ts index 9476d2f47d..1a69f55b7f 100644 --- a/packages/sitecore-jss/src/graphql/index.ts +++ b/packages/sitecore-jss/src/graphql/index.ts @@ -1,5 +1,7 @@ export { getAppRootId, AppRootQueryResult } from './app-root-query'; export { + RetryStrategy, + DefaultRetryStrategy, GraphQLClient, GraphQLRequestClient, GraphQLRequestClientConfig, diff --git a/packages/sitecore-jss/src/i18n/graphql-dictionary-service.ts b/packages/sitecore-jss/src/i18n/graphql-dictionary-service.ts index 88a865d26c..dd924d82df 100644 --- a/packages/sitecore-jss/src/i18n/graphql-dictionary-service.ts +++ b/packages/sitecore-jss/src/i18n/graphql-dictionary-service.ts @@ -56,7 +56,7 @@ const query = /* GraphQL */ ` export interface GraphQLDictionaryServiceConfig extends SearchServiceConfig, CacheOptions, - Pick { + Pick { /** * The URL of the graphQL endpoint. * @deprecated use @param clientFactory property instead diff --git a/packages/sitecore-jss/src/index.ts b/packages/sitecore-jss/src/index.ts index bf1a5a8791..6debc3aa2a 100644 --- a/packages/sitecore-jss/src/index.ts +++ b/packages/sitecore-jss/src/index.ts @@ -5,6 +5,8 @@ import * as constants from './constants'; export { default as debug, Debugger, enableDebug } from './debug'; export { HttpDataFetcher, HttpResponse, fetchData } from './data-fetcher'; export { + RetryStrategy, + DefaultRetryStrategy, GraphQLClient, GraphQLRequestClient, GraphQLRequestClientConfig, diff --git a/packages/sitecore-jss/src/layout/graphql-layout-service.ts b/packages/sitecore-jss/src/layout/graphql-layout-service.ts index 7ad50aa41a..2fade8bfae 100644 --- a/packages/sitecore-jss/src/layout/graphql-layout-service.ts +++ b/packages/sitecore-jss/src/layout/graphql-layout-service.ts @@ -8,7 +8,8 @@ import { } from '../graphql-request-client'; import debug from '../debug'; -export interface GraphQLLayoutServiceConfig extends Pick { +export interface GraphQLLayoutServiceConfig + extends Pick { /** * Your Graphql endpoint * @deprecated use @param clientFactory property instead diff --git a/yarn.lock b/yarn.lock index 9b7dedf388..1bf7975080 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5256,6 +5256,15 @@ __metadata: languageName: node linkType: hard +"@sinonjs/fake-timers@npm:^11.2.2": + version: 11.2.2 + resolution: "@sinonjs/fake-timers@npm:11.2.2" + dependencies: + "@sinonjs/commons": ^3.0.0 + checksum: 68c29b0e1856fdc280df03ddbf57c726420b78e9f943a241b471edc018fb14ff36fdc1daafd6026cba08c3c7f50c976fb7ae11b88ff44cd7f609692ca7d25158 + languageName: node + linkType: hard + "@sinonjs/fake-timers@npm:^7.1.0": version: 7.1.2 resolution: "@sinonjs/fake-timers@npm:7.1.2" @@ -5276,7 +5285,7 @@ __metadata: languageName: node linkType: hard -"@sinonjs/text-encoding@npm:^0.7.1": +"@sinonjs/text-encoding@npm:^0.7.1, @sinonjs/text-encoding@npm:^0.7.2": version: 0.7.2 resolution: "@sinonjs/text-encoding@npm:0.7.2" checksum: fe690002a32ba06906cf87e2e8fe84d1590294586f2a7fd180a65355b53660c155c3273d8011a5f2b77209b819aa7306678ae6e4aea0df014bd7ffd4bbbcf1ab @@ -5756,6 +5765,7 @@ __metadata: "@types/memory-cache": ^0.2.1 "@types/mocha": ^9.0.0 "@types/node": ^16.11.6 + "@types/sinon": ^17.0.3 "@types/url-parse": 1.4.8 axios: ^0.21.1 chai: ^4.2.0 @@ -5772,6 +5782,7 @@ __metadata: mocha: ^10.2.0 nock: ^13.0.5 nyc: ^15.1.0 + sinon: ^17.0.1 ts-node: ^8.4.1 tslib: ^1.10.0 typescript: ~4.3.5 @@ -6729,6 +6740,15 @@ __metadata: languageName: node linkType: hard +"@types/sinon@npm:^17.0.3": + version: 17.0.3 + resolution: "@types/sinon@npm:17.0.3" + dependencies: + "@types/sinonjs__fake-timers": "*" + checksum: c8e9956d9c90fe1ec1cc43085ae48897f93f9ea86e909ab47f255ea71f5229651faa070393950fb6923aef426c84e92b375503f9f8886ef44668b82a8ee49e9a + languageName: node + linkType: hard + "@types/sinonjs__fake-timers@npm:*": version: 8.1.5 resolution: "@types/sinonjs__fake-timers@npm:8.1.5" @@ -17856,6 +17876,13 @@ __metadata: languageName: node linkType: hard +"just-extend@npm:^6.2.0": + version: 6.2.0 + resolution: "just-extend@npm:6.2.0" + checksum: 022024d6f687c807963b97a24728a378799f7e4af7357d1c1f90dedb402943d5c12be99a5136654bed8362c37a358b1793feaad3366896f239a44e17c5032d86 + languageName: node + linkType: hard + "karma-chrome-launcher@npm:^3.1.0": version: 3.2.0 resolution: "karma-chrome-launcher@npm:3.2.0" @@ -19888,6 +19915,19 @@ __metadata: languageName: node linkType: hard +"nise@npm:^5.1.5": + version: 5.1.9 + resolution: "nise@npm:5.1.9" + dependencies: + "@sinonjs/commons": ^3.0.0 + "@sinonjs/fake-timers": ^11.2.2 + "@sinonjs/text-encoding": ^0.7.2 + just-extend: ^6.2.0 + path-to-regexp: ^6.2.1 + checksum: ab9fd6eabc98170f18aef6c9567983145c1dc62c7aef46eda0fea754083316c1f0f9b2c32e9b4bfdd25122276d670293596ed672b54dd1ffa8eb58b56a30ea95 + languageName: node + linkType: hard + "nocache@npm:^2.1.0": version: 2.1.0 resolution: "nocache@npm:2.1.0" @@ -21338,6 +21378,13 @@ __metadata: languageName: node linkType: hard +"path-to-regexp@npm:^6.2.1": + version: 6.2.1 + resolution: "path-to-regexp@npm:6.2.1" + checksum: f0227af8284ea13300f4293ba111e3635142f976d4197f14d5ad1f124aebd9118783dd2e5f1fe16f7273743cc3dbeddfb7493f237bb27c10fdae07020cc9b698 + languageName: node + linkType: hard + "path-type@npm:^3.0.0": version: 3.0.0 resolution: "path-type@npm:3.0.0" @@ -23654,6 +23701,20 @@ __metadata: languageName: node linkType: hard +"sinon@npm:^17.0.1": + version: 17.0.1 + resolution: "sinon@npm:17.0.1" + dependencies: + "@sinonjs/commons": ^3.0.0 + "@sinonjs/fake-timers": ^11.2.2 + "@sinonjs/samsam": ^8.0.0 + diff: ^5.1.0 + nise: ^5.1.5 + supports-color: ^7.2.0 + checksum: a807c2997d6eabdcaa4409df9fd9816a3e839f96d7e5d76610a33f5e1b60cf37616c6288f0f580262da17ea4ee626c6d1600325bf423e30c5a7f0d9a203e26c0 + languageName: node + linkType: hard + "sisteransi@npm:^1.0.5": version: 1.0.5 resolution: "sisteransi@npm:1.0.5"