From be4836b63b2eea12885bd6d6efebd71a99d3d1a5 Mon Sep 17 00:00:00 2001 From: anitarua Date: Thu, 25 Jul 2024 16:50:20 -0700 Subject: [PATCH 01/43] feat: add retry strategy to storage client --- .../storage-default-eligibility-strategy.ts | 70 +++++++++++++++++++ .../retry/storage-default-retry-strategy.ts | 45 ++++++++++++ .../src/config/storage-configuration.ts | 30 +++++++- .../src/config/storage-configurations.ts | 11 +++ .../transport/storage/transport-strategy.ts | 37 ++++++++++ 5 files changed, 191 insertions(+), 2 deletions(-) create mode 100644 packages/client-sdk-nodejs/src/config/retry/storage-default-eligibility-strategy.ts create mode 100644 packages/client-sdk-nodejs/src/config/retry/storage-default-retry-strategy.ts diff --git a/packages/client-sdk-nodejs/src/config/retry/storage-default-eligibility-strategy.ts b/packages/client-sdk-nodejs/src/config/retry/storage-default-eligibility-strategy.ts new file mode 100644 index 000000000..f4a81070c --- /dev/null +++ b/packages/client-sdk-nodejs/src/config/retry/storage-default-eligibility-strategy.ts @@ -0,0 +1,70 @@ +import {MomentoLogger, MomentoLoggerFactory} from '../../'; +import {Status} from '@grpc/grpc-js/build/src/constants'; +import { + EligibilityStrategy, + EligibleForRetryProps, +} from './eligibility-strategy'; + +const retryableGrpcStatusCodes: Array = [ + // including all the status codes for reference, but + // commenting out the ones we don't want to retry on for now. + + // Status.OK, + // Status.CANCELLED, + // Status.UNKNOWN, + // Status.INVALID_ARGUMENT, + Status.DEADLINE_EXCEEDED, + // Status.NOT_FOUND, + // Status.ALREADY_EXISTS, + // Status.PERMISSION_DENIED, + // Status.RESOURCE_EXHAUSTED, + // Status.FAILED_PRECONDITION, + // Status.ABORTED, + // Status.OUT_OF_RANGE, + // Status.UNIMPLEMENTED, + Status.INTERNAL, + Status.UNAVAILABLE, + // Status.DATA_LOSS, + // Status.UNAUTHENTICATED +]; + +// TODO: verify correct strings. Should be proto file's "./" right? +const retryableRequestTypes: Array = [ + '/store.Store/Put', + '/store.Store.?/Get', + '/store.Store.?/Delete', +]; + +export class DefaultStorageEligibilityStrategy implements EligibilityStrategy { + private readonly logger: MomentoLogger; + + constructor(loggerFactory: MomentoLoggerFactory) { + this.logger = loggerFactory.getLogger(this); + } + + isEligibleForRetry(props: EligibleForRetryProps): boolean { + if (!retryableGrpcStatusCodes.includes(props.grpcStatus.code)) { + this.logger.debug( + `Response with status code ${props.grpcStatus.code} is not retryable.` + ); + return false; + } + + const errorMetadata = props.grpcStatus.metadata.get('err'); + // Expecting only one error condition in metadata + if (errorMetadata.length === 1) { + // TODO: check error metadata for specific conditions that should not be retried + if (errorMetadata[0] === 'unacceptable retry condition') return false; + } else { + // If no metadata, fall back to checking idemopotency + if (!retryableRequestTypes.includes(props.grpcRequest.path)) { + this.logger.debug( + `Request with type ${props.grpcRequest.path} is not retryable.` + ); + return false; + } + } + + return true; + } +} diff --git a/packages/client-sdk-nodejs/src/config/retry/storage-default-retry-strategy.ts b/packages/client-sdk-nodejs/src/config/retry/storage-default-retry-strategy.ts new file mode 100644 index 000000000..4aa959384 --- /dev/null +++ b/packages/client-sdk-nodejs/src/config/retry/storage-default-retry-strategy.ts @@ -0,0 +1,45 @@ +import { + DeterminewhenToRetryRequestProps, + RetryStrategy, +} from './retry-strategy'; +import {EligibilityStrategy} from './eligibility-strategy'; +import {DefaultEligibilityStrategy} from './default-eligibility-strategy'; +import {MomentoLoggerFactory, MomentoLogger} from '../../'; + +export interface DefaultStorageRetryStrategyProps { + loggerFactory: MomentoLoggerFactory; + eligibilityStrategy?: EligibilityStrategy; + // Retry request after a fixed time interval (defaults to 100ms) + retryDelayInterval?: number; +} + +export class DefaultStorageRetryStrategy implements RetryStrategy { + private readonly logger: MomentoLogger; + private readonly eligibilityStrategy: EligibilityStrategy; + private readonly retryDelayInterval: number; + + constructor(props: DefaultStorageRetryStrategyProps) { + this.logger = props.loggerFactory.getLogger(this); + this.eligibilityStrategy = + props.eligibilityStrategy ?? + new DefaultEligibilityStrategy(props.loggerFactory); + this.retryDelayInterval = props.retryDelayInterval ?? 100; + } + + determineWhenToRetryRequest( + props: DeterminewhenToRetryRequestProps + ): number | null { + this.logger.debug( + `Determining whether request is eligible for retry; status code: ${props.grpcStatus.code}, request type: ${props.grpcRequest.path}, attemptNumber: ${props.attemptNumber}` + ); + if (!this.eligibilityStrategy.isEligibleForRetry(props)) { + // null means do not retry + return null; + } + this.logger.debug( + `Request is eligible for retry (attempt ${props.attemptNumber}), retrying immediately.` + ); + // retry after a fixed time interval has passed + return this.retryDelayInterval; + } +} diff --git a/packages/client-sdk-nodejs/src/config/storage-configuration.ts b/packages/client-sdk-nodejs/src/config/storage-configuration.ts index 459eb5849..1e2df88b8 100644 --- a/packages/client-sdk-nodejs/src/config/storage-configuration.ts +++ b/packages/client-sdk-nodejs/src/config/storage-configuration.ts @@ -1,6 +1,9 @@ -import {MomentoLoggerFactory} from '../'; +import {MomentoLoggerFactory, RetryStrategy} from '../'; import {StorageTransportStrategy} from './transport/storage'; +// TODO: include ResponseDataReceivedTimeout as a top-level config? +// Or keep at the StorageTransportStrategy level? + /** * Configuration options for Momento StorageClient * @@ -28,11 +31,18 @@ export interface StorageConfiguration { /** * Copy constructor for overriding TransportStrategy * @param {StorageTransportStrategy} transportStrategy - * @returns {Configuration} a new Configuration object with the specified TransportStrategy + * @returns {StorageConfiguration} a new Configuration object with the specified TransportStrategy */ withTransportStrategy( transportStrategy: StorageTransportStrategy ): StorageConfiguration; + + /** + * Copy constructor for overriding RetryStrategy + * @param {RetryStrategy} retryStrategy + * @returns {StorageConfiguration} a new Configuration object with the specified RetryStrategy + */ + withRetryStrategy(retryStrategy: RetryStrategy): StorageConfiguration; } export interface StorageConfigurationProps { @@ -44,15 +54,21 @@ export interface StorageConfigurationProps { * Configures low-level options for network interactions with the Momento service */ transportStrategy: StorageTransportStrategy; + /** + * Configures how and when failed requests will be retried + */ + retryStrategy: RetryStrategy; } export class StorageClientConfiguration implements StorageConfiguration { private readonly loggerFactory: MomentoLoggerFactory; private readonly transportStrategy: StorageTransportStrategy; + private readonly retryStrategy: RetryStrategy; constructor(props: StorageConfigurationProps) { this.loggerFactory = props.loggerFactory; this.transportStrategy = props.transportStrategy; + this.retryStrategy = props.retryStrategy; } getLoggerFactory(): MomentoLoggerFactory { @@ -68,6 +84,7 @@ export class StorageClientConfiguration implements StorageConfiguration { loggerFactory: this.loggerFactory, transportStrategy: this.transportStrategy.withClientTimeoutMillis(clientTimeoutMillis), + retryStrategy: this.retryStrategy, }); } @@ -77,6 +94,15 @@ export class StorageClientConfiguration implements StorageConfiguration { return new StorageClientConfiguration({ loggerFactory: this.loggerFactory, transportStrategy: transportStrategy, + retryStrategy: this.retryStrategy, + }); + } + + withRetryStrategy(retryStrategy: RetryStrategy): StorageConfiguration { + return new StorageClientConfiguration({ + loggerFactory: this.loggerFactory, + transportStrategy: this.transportStrategy, + retryStrategy: retryStrategy, }); } } diff --git a/packages/client-sdk-nodejs/src/config/storage-configurations.ts b/packages/client-sdk-nodejs/src/config/storage-configurations.ts index 26763b3ae..458ae5989 100644 --- a/packages/client-sdk-nodejs/src/config/storage-configurations.ts +++ b/packages/client-sdk-nodejs/src/config/storage-configurations.ts @@ -8,10 +8,20 @@ import { StaticStorageGrpcConfiguration, StaticStorageTransportStrategy, } from './transport/storage'; +import {DefaultStorageRetryStrategy} from './retry/storage-default-retry-strategy'; +import {RetryStrategy} from './retry/retry-strategy'; const defaultLoggerFactory: MomentoLoggerFactory = new DefaultMomentoLoggerFactory(); +function defaultRetryStrategy( + loggerFactory: MomentoLoggerFactory +): RetryStrategy { + return new DefaultStorageRetryStrategy({ + loggerFactory: loggerFactory, + }); +} + /** * Laptop config provides defaults suitable for a medium-to-high-latency dev environment. * @export @@ -34,6 +44,7 @@ export class Laptop extends StorageClientConfiguration { deadlineMillis: 5000, }), }), + retryStrategy: defaultRetryStrategy(loggerFactory), }); } } diff --git a/packages/client-sdk-nodejs/src/config/transport/storage/transport-strategy.ts b/packages/client-sdk-nodejs/src/config/transport/storage/transport-strategy.ts index e3c45f01a..8ebe34799 100644 --- a/packages/client-sdk-nodejs/src/config/transport/storage/transport-strategy.ts +++ b/packages/client-sdk-nodejs/src/config/transport/storage/transport-strategy.ts @@ -11,6 +11,12 @@ export interface StorageTransportStrategy { */ getGrpcConfig(): StorageGrpcConfiguration; + /** + * Configures time to wait for response data to be received before retrying (defaults to 1s). + * @returns {StorageGrpcConfiguration} + */ + getResponseDataReceivedTimeout(): number; + /** * Copy constructor for overriding the gRPC configuration * @param {TopicGrpcConfiguration} grpcConfig @@ -28,6 +34,15 @@ export interface StorageTransportStrategy { withClientTimeoutMillis( clientTimeoutMillis: number ): StorageTransportStrategy; + + /** + * Copy constructor to update the ResponseDataReceivedTimeout + * @param {number} responseDataReceivedTimeout + * @returns {StorageTransportStrategy} a new StorageTransportStrategy with the specified client timeout + */ + withResponseDataReceivedTimeout( + responseDataReceivedTimeout: number + ): StorageTransportStrategy; } export interface StorageTransportStrategyProps { @@ -35,6 +50,10 @@ export interface StorageTransportStrategyProps { * low-level gRPC settings for communication with the Momento server */ grpcConfiguration: StorageGrpcConfiguration; + /** + * time to wait for response data to be received before retrying (defaults to 1s) + */ + responseDataReceivedTimeout?: number; } export class StaticStorageGrpcConfiguration @@ -61,20 +80,28 @@ export class StaticStorageTransportStrategy implements StorageTransportStrategy { private readonly grpcConfig: StorageGrpcConfiguration; + private readonly responseDataReceivedTimeout: number; constructor(props: StorageTransportStrategyProps) { this.grpcConfig = props.grpcConfiguration; + this.responseDataReceivedTimeout = + props.responseDataReceivedTimeout ?? 1000; } getGrpcConfig(): StorageGrpcConfiguration { return this.grpcConfig; } + getResponseDataReceivedTimeout(): number { + return this.responseDataReceivedTimeout; + } + withGrpcConfig( grpcConfig: StorageGrpcConfiguration ): StorageTransportStrategy { return new StaticStorageTransportStrategy({ grpcConfiguration: grpcConfig, + responseDataReceivedTimeout: this.responseDataReceivedTimeout, }); } @@ -84,6 +111,16 @@ export class StaticStorageTransportStrategy return new StaticStorageTransportStrategy({ grpcConfiguration: this.grpcConfig.withDeadlineMillis(clientTimeoutMillis), + responseDataReceivedTimeout: this.responseDataReceivedTimeout, + }); + } + + withResponseDataReceivedTimeout( + responseDataReceivedTimeout: number + ): StorageTransportStrategy { + return new StaticStorageTransportStrategy({ + grpcConfiguration: this.grpcConfig, + responseDataReceivedTimeout: responseDataReceivedTimeout, }); } } From 536d334b4363ad30a6a1675d2f4bb75c567124b5 Mon Sep 17 00:00:00 2001 From: anitarua Date: Fri, 26 Jul 2024 14:04:07 -0700 Subject: [PATCH 02/43] implement mechanism for 1s timeouts+retries up until 5s overall timeout --- .../storage-default-eligibility-strategy.ts | 4 +- .../retry/storage-default-retry-strategy.ts | 6 +- .../src/config/storage-configuration.ts | 9 +++ .../storage-client-timeout-interceptor.ts | 70 +++++++++++++++++++ .../src/internal/storage-data-client.ts | 34 ++++++++- .../test/integration/integration-setup.ts | 6 +- 6 files changed, 120 insertions(+), 9 deletions(-) create mode 100644 packages/client-sdk-nodejs/src/internal/grpc/storage-client-timeout-interceptor.ts diff --git a/packages/client-sdk-nodejs/src/config/retry/storage-default-eligibility-strategy.ts b/packages/client-sdk-nodejs/src/config/retry/storage-default-eligibility-strategy.ts index f4a81070c..646c7d31a 100644 --- a/packages/client-sdk-nodejs/src/config/retry/storage-default-eligibility-strategy.ts +++ b/packages/client-sdk-nodejs/src/config/retry/storage-default-eligibility-strategy.ts @@ -31,8 +31,8 @@ const retryableGrpcStatusCodes: Array = [ // TODO: verify correct strings. Should be proto file's "./" right? const retryableRequestTypes: Array = [ '/store.Store/Put', - '/store.Store.?/Get', - '/store.Store.?/Delete', + '/store.Store/Get', + '/store.Store/Delete', ]; export class DefaultStorageEligibilityStrategy implements EligibilityStrategy { diff --git a/packages/client-sdk-nodejs/src/config/retry/storage-default-retry-strategy.ts b/packages/client-sdk-nodejs/src/config/retry/storage-default-retry-strategy.ts index 4aa959384..3f703ea51 100644 --- a/packages/client-sdk-nodejs/src/config/retry/storage-default-retry-strategy.ts +++ b/packages/client-sdk-nodejs/src/config/retry/storage-default-retry-strategy.ts @@ -3,8 +3,8 @@ import { RetryStrategy, } from './retry-strategy'; import {EligibilityStrategy} from './eligibility-strategy'; -import {DefaultEligibilityStrategy} from './default-eligibility-strategy'; import {MomentoLoggerFactory, MomentoLogger} from '../../'; +import {DefaultStorageEligibilityStrategy} from './storage-default-eligibility-strategy'; export interface DefaultStorageRetryStrategyProps { loggerFactory: MomentoLoggerFactory; @@ -22,7 +22,7 @@ export class DefaultStorageRetryStrategy implements RetryStrategy { this.logger = props.loggerFactory.getLogger(this); this.eligibilityStrategy = props.eligibilityStrategy ?? - new DefaultEligibilityStrategy(props.loggerFactory); + new DefaultStorageEligibilityStrategy(props.loggerFactory); this.retryDelayInterval = props.retryDelayInterval ?? 100; } @@ -37,7 +37,7 @@ export class DefaultStorageRetryStrategy implements RetryStrategy { return null; } this.logger.debug( - `Request is eligible for retry (attempt ${props.attemptNumber}), retrying immediately.` + `Request is eligible for retry (attempt ${props.attemptNumber}), retrying soon.` ); // retry after a fixed time interval has passed return this.retryDelayInterval; diff --git a/packages/client-sdk-nodejs/src/config/storage-configuration.ts b/packages/client-sdk-nodejs/src/config/storage-configuration.ts index 1e2df88b8..624faa2e7 100644 --- a/packages/client-sdk-nodejs/src/config/storage-configuration.ts +++ b/packages/client-sdk-nodejs/src/config/storage-configuration.ts @@ -21,6 +21,11 @@ export interface StorageConfiguration { */ getTransportStrategy(): StorageTransportStrategy; + /** + * @returns {RetryStrategy} the current configuration options for how and when failed requests will be retried + */ + getRetryStrategy(): RetryStrategy; + /** * Convenience copy constructor that updates the client-side timeout setting in the TransportStrategy * @param {number} clientTimeoutMillis @@ -79,6 +84,10 @@ export class StorageClientConfiguration implements StorageConfiguration { return this.transportStrategy; } + getRetryStrategy(): RetryStrategy { + return this.retryStrategy; + } + withClientTimeoutMillis(clientTimeoutMillis: number): StorageConfiguration { return new StorageClientConfiguration({ loggerFactory: this.loggerFactory, diff --git a/packages/client-sdk-nodejs/src/internal/grpc/storage-client-timeout-interceptor.ts b/packages/client-sdk-nodejs/src/internal/grpc/storage-client-timeout-interceptor.ts new file mode 100644 index 000000000..e00a8bfa8 --- /dev/null +++ b/packages/client-sdk-nodejs/src/internal/grpc/storage-client-timeout-interceptor.ts @@ -0,0 +1,70 @@ +import {MomentoLogger, MomentoLoggerFactory} from '@gomomento/sdk-core'; +import {InterceptingCall, Interceptor, status} from '@grpc/grpc-js'; + +export function createStorageClientTimeoutInterceptor( + loggerFactory: MomentoLoggerFactory, + overallRequestTimeoutMs: number, + responseDataReceivedTimeoutMs: number +): Array { + return [ + new StorageClientTimeoutInterceptor( + loggerFactory, + overallRequestTimeoutMs, + responseDataReceivedTimeoutMs + ).createTimeoutInterceptor(), + ]; +} + +export class StorageClientTimeoutInterceptor { + private readonly logger: MomentoLogger; + private readonly responseDataReceivedTimeoutMs: number; + private readonly overallDeadline: number; + + constructor( + loggerFactory: MomentoLoggerFactory, + overallRequestTimeoutMs: number, + responseDataReceivedTimeoutMs: number + ) { + this.logger = loggerFactory.getLogger(this); + this.responseDataReceivedTimeoutMs = responseDataReceivedTimeoutMs; + const newDate = new Date(Date.now()); + this.overallDeadline = newDate.setMilliseconds( + newDate.getMilliseconds() + overallRequestTimeoutMs + ); + this.logger.debug( + `creating interceptor, overall deadline: ${this.overallDeadline}` + ); + } + + public createTimeoutInterceptor(): Interceptor { + return (options, nextCall) => { + this.logger.debug( + `intercepting call with options.deadline ${ + options.deadline?.valueOf() || 'undefined' + }` + ); + + const deadline = new Date(Date.now()); + deadline.setMilliseconds( + deadline.getMilliseconds() + this.responseDataReceivedTimeoutMs + ); + + if (deadline.valueOf() > this.overallDeadline.valueOf()) { + this.logger.debug( + 'Unable to successfully retry request within client timeout, canceling request' + ); + const call = new InterceptingCall(nextCall(options)); + call.cancelWithStatus( + status.CANCELLED, + 'Unable to successfully retry request within client timeout' + ); + return call; + } else { + options.deadline = deadline; + this.logger.debug(`new deadline set to ${options.deadline.valueOf()}`); + } + + return new InterceptingCall(nextCall(options)); + }; + } +} diff --git a/packages/client-sdk-nodejs/src/internal/storage-data-client.ts b/packages/client-sdk-nodejs/src/internal/storage-data-client.ts index bca006633..1e8af82a6 100644 --- a/packages/client-sdk-nodejs/src/internal/storage-data-client.ts +++ b/packages/client-sdk-nodejs/src/internal/storage-data-client.ts @@ -26,6 +26,8 @@ import {StorageConfiguration} from '../config/storage-configuration'; import {StorageClientPropsWithConfig} from './storage-client-props-with-config'; import {StaticGrpcConfiguration} from '../config/transport/cache'; import {CacheServiceErrorMapper} from '../errors/cache-service-error-mapper'; +import {createStorageClientTimeoutInterceptor} from './grpc/storage-client-timeout-interceptor'; +import {createRetryInterceptorIfEnabled} from './grpc/retry-interceptor'; export class StorageDataClient implements IStorageDataClient { private readonly configuration: StorageConfiguration; @@ -36,6 +38,7 @@ export class StorageDataClient implements IStorageDataClient { private readonly client: store.StoreClient; private readonly interceptors: Interceptor[]; private static readonly DEFAULT_MAX_SESSION_MEMORY_MB: number = 256; + private readonly responseDataReceivedTimeoutMs: number; /** * @param {StorageClientPropsWithConfig} props @@ -49,6 +52,9 @@ export class StorageDataClient implements IStorageDataClient { .getTransportStrategy() .getGrpcConfig() .getDeadlineMillis(); + this.responseDataReceivedTimeoutMs = this.configuration + .getTransportStrategy() + .getResponseDataReceivedTimeout(); this.validateRequestTimeout(this.requestTimeoutMs); this.logger.debug( `Creating leaderboard client using endpoint: '${this.credentialProvider.getStorageEndpoint()}'` @@ -112,6 +118,10 @@ export class StorageDataClient implements IStorageDataClient { new Header('runtime-version', `nodejs:${process.versions.node}`), ]; return [ + ...createRetryInterceptorIfEnabled( + this.configuration.getLoggerFactory(), + this.configuration.getRetryStrategy() + ), new HeaderInterceptorProvider(headers).createHeadersInterceptor(), ClientTimeoutInterceptor(this.requestTimeoutMs), ]; @@ -154,7 +164,13 @@ export class StorageDataClient implements IStorageDataClient { request, metadata, { - interceptors: this.interceptors, + interceptors: this.interceptors.concat( + createStorageClientTimeoutInterceptor( + this.configuration.getLoggerFactory(), + this.requestTimeoutMs, + this.responseDataReceivedTimeoutMs + ) + ), }, (err: ServiceError | null, resp) => { const value = resp?.value?.value; @@ -277,7 +293,13 @@ export class StorageDataClient implements IStorageDataClient { request, metadata, { - interceptors: this.interceptors, + interceptors: this.interceptors.concat( + createStorageClientTimeoutInterceptor( + this.configuration.getLoggerFactory(), + this.requestTimeoutMs, + this.responseDataReceivedTimeoutMs + ) + ), }, (err: ServiceError | null, resp) => { if (resp) { @@ -326,7 +348,13 @@ export class StorageDataClient implements IStorageDataClient { request, metadata, { - interceptors: this.interceptors, + interceptors: this.interceptors.concat( + createStorageClientTimeoutInterceptor( + this.configuration.getLoggerFactory(), + this.requestTimeoutMs, + this.responseDataReceivedTimeoutMs + ) + ), }, (err: ServiceError | null, resp) => { if (resp) { diff --git a/packages/client-sdk-nodejs/test/integration/integration-setup.ts b/packages/client-sdk-nodejs/test/integration/integration-setup.ts index 9d3645557..ce68ecd0f 100644 --- a/packages/client-sdk-nodejs/test/integration/integration-setup.ts +++ b/packages/client-sdk-nodejs/test/integration/integration-setup.ts @@ -13,6 +13,8 @@ import { LeaderboardConfigurations, PreviewStorageClient, StorageConfigurations, + DefaultMomentoLoggerFactory, + DefaultMomentoLoggerLevel, } from '../../src'; import {ICacheClient} from '@gomomento/sdk-core/dist/src/clients/ICacheClient'; import {ITopicClient} from '@gomomento/sdk-core/dist/src/clients/ITopicClient'; @@ -149,7 +151,9 @@ function momentoTopicClientForTesting(): TopicClient { function momentoStorageClientForTesting(): PreviewStorageClient { return new PreviewStorageClient({ - configuration: StorageConfigurations.Laptop.latest(), + configuration: StorageConfigurations.Laptop.latest( + new DefaultMomentoLoggerFactory(DefaultMomentoLoggerLevel.DEBUG) + ), credentialProvider: integrationTestCacheClientProps().credentialProvider, }); } From 855d5034d2236a6a6f57fe09658015005959db3a Mon Sep 17 00:00:00 2001 From: anitarua Date: Wed, 31 Jul 2024 13:36:05 -0700 Subject: [PATCH 03/43] add some unit tests for storage retry strategy --- .../test/unit/storage-retry.test.ts | 160 ++++++++++++++++++ 1 file changed, 160 insertions(+) create mode 100644 packages/client-sdk-nodejs/test/unit/storage-retry.test.ts diff --git a/packages/client-sdk-nodejs/test/unit/storage-retry.test.ts b/packages/client-sdk-nodejs/test/unit/storage-retry.test.ts new file mode 100644 index 000000000..380aac5ba --- /dev/null +++ b/packages/client-sdk-nodejs/test/unit/storage-retry.test.ts @@ -0,0 +1,160 @@ +import {Status} from '@grpc/grpc-js/build/src/constants'; +import { + ClientMethodDefinition, + Deserialize, + Serialize, +} from '@grpc/grpc-js/build/src/make-client'; +import { + DefaultMomentoLoggerFactory, + FixedCountRetryStrategy, + StaticStorageGrpcConfiguration, + StaticStorageTransportStrategy, + StorageClientConfiguration, +} from '../../src'; +import {DefaultStorageRetryStrategy} from '../../src/config/retry/storage-default-retry-strategy'; +import {Metadata, StatusObject} from '@grpc/grpc-js'; + +describe('storage configuration', () => { + const testLoggerFactory = new DefaultMomentoLoggerFactory(); + const testGrpcConfiguration = new StaticStorageGrpcConfiguration({ + deadlineMillis: 5000, + }); + const testTransportStrategy = new StaticStorageTransportStrategy({ + grpcConfiguration: testGrpcConfiguration, + responseDataReceivedTimeout: 1000, + }); + const testRetryStrategy = new DefaultStorageRetryStrategy({ + loggerFactory: testLoggerFactory, + }); + const testConfiguration = new StorageClientConfiguration({ + loggerFactory: testLoggerFactory, + transportStrategy: testTransportStrategy, + retryStrategy: testRetryStrategy, + }); + + it('should support overriding retry strategy', () => { + const newRetryStrategy = new FixedCountRetryStrategy({ + loggerFactory: testLoggerFactory, + maxAttempts: 2, + }); + const configWithNewRetryStrategy = + testConfiguration.withRetryStrategy(newRetryStrategy); + expect(configWithNewRetryStrategy.getLoggerFactory()).toEqual( + testLoggerFactory + ); + expect(configWithNewRetryStrategy.getRetryStrategy()).toEqual( + newRetryStrategy + ); + expect(configWithNewRetryStrategy.getTransportStrategy()).toEqual( + testTransportStrategy + ); + }); + + it('should support overriding transport strategy', () => { + const newTransportStrategy = new StaticStorageTransportStrategy({ + grpcConfiguration: testGrpcConfiguration, + responseDataReceivedTimeout: 5000, + }); + const configWithNewTransportStrategy = + testConfiguration.withTransportStrategy(newTransportStrategy); + expect(configWithNewTransportStrategy.getLoggerFactory()).toEqual( + testLoggerFactory + ); + expect(configWithNewTransportStrategy.getRetryStrategy()).toEqual( + testRetryStrategy + ); + expect(configWithNewTransportStrategy.getTransportStrategy()).toEqual( + newTransportStrategy + ); + }); + + it('default storage retry strategy retries when eligible', () => { + function testSerialize(value: string): Buffer { + return Buffer.from(value); + } + const sFunc: Serialize = testSerialize; + + function TestDeserialize(bytes: Buffer): string { + return bytes.toString(); + } + const dFunc: Deserialize = TestDeserialize; + + const testClientRequest: ClientMethodDefinition = { + path: '/store.Store/Put', + requestStream: false, + responseStream: false, + requestSerialize: sFunc, + responseDeserialize: dFunc, + }; + + const testGrpcStatusDeadlineExceeded: StatusObject = { + code: Status.DEADLINE_EXCEEDED, + details: 'deadline exceeded', + metadata: new Metadata(), + }; + expect( + testRetryStrategy + .determineWhenToRetryRequest({ + grpcStatus: testGrpcStatusDeadlineExceeded, + grpcRequest: testClientRequest as ClientMethodDefinition< + unknown, + unknown + >, + attemptNumber: 1, + }) + ?.valueOf() + ).toEqual(100); // will retry after 100ms + + const testGrpcStatusInternalError = { + code: Status.INTERNAL, + details: 'internal server error', + metadata: new Metadata(), + }; + expect( + testRetryStrategy + .determineWhenToRetryRequest({ + grpcStatus: testGrpcStatusInternalError, + grpcRequest: testClientRequest as ClientMethodDefinition< + unknown, + unknown + >, + attemptNumber: 1, + }) + ?.valueOf() + ).toEqual(100); // will retry after 100ms + + const testGrpcStatusUnavailable = { + code: Status.UNAVAILABLE, + details: 'server unavailable', + metadata: new Metadata(), + }; + expect( + testRetryStrategy + .determineWhenToRetryRequest({ + grpcStatus: testGrpcStatusUnavailable, + grpcRequest: testClientRequest as ClientMethodDefinition< + unknown, + unknown + >, + attemptNumber: 1, + }) + ?.valueOf() + ).toEqual(100); // will retry after 100ms + + const testGrpcStatusCancelled = { + code: Status.CANCELLED, + details: 'cancelled', + metadata: new Metadata(), + }; + expect( + testRetryStrategy.determineWhenToRetryRequest({ + grpcStatus: testGrpcStatusCancelled, + grpcRequest: testClientRequest as ClientMethodDefinition< + unknown, + unknown + >, + attemptNumber: 1, + }) + ).toBeNull(); // will not retry + }); +}); From d7c826f6115e9508ab2f29357ceafd54c40ac178 Mon Sep 17 00:00:00 2001 From: anitarua Date: Wed, 31 Jul 2024 13:41:45 -0700 Subject: [PATCH 04/43] remove debug logger from integration test setup --- .../client-sdk-nodejs/test/integration/integration-setup.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/client-sdk-nodejs/test/integration/integration-setup.ts b/packages/client-sdk-nodejs/test/integration/integration-setup.ts index ce68ecd0f..9d3645557 100644 --- a/packages/client-sdk-nodejs/test/integration/integration-setup.ts +++ b/packages/client-sdk-nodejs/test/integration/integration-setup.ts @@ -13,8 +13,6 @@ import { LeaderboardConfigurations, PreviewStorageClient, StorageConfigurations, - DefaultMomentoLoggerFactory, - DefaultMomentoLoggerLevel, } from '../../src'; import {ICacheClient} from '@gomomento/sdk-core/dist/src/clients/ICacheClient'; import {ITopicClient} from '@gomomento/sdk-core/dist/src/clients/ITopicClient'; @@ -151,9 +149,7 @@ function momentoTopicClientForTesting(): TopicClient { function momentoStorageClientForTesting(): PreviewStorageClient { return new PreviewStorageClient({ - configuration: StorageConfigurations.Laptop.latest( - new DefaultMomentoLoggerFactory(DefaultMomentoLoggerLevel.DEBUG) - ), + configuration: StorageConfigurations.Laptop.latest(), credentialProvider: integrationTestCacheClientProps().credentialProvider, }); } From c4101860bd1af47853b58f936e1f8a67c70ced44 Mon Sep 17 00:00:00 2001 From: anitarua Date: Wed, 31 Jul 2024 15:54:31 -0700 Subject: [PATCH 05/43] reset deadline only after last one has passed --- .../storage-client-timeout-interceptor.ts | 73 ++++++++++++------- 1 file changed, 46 insertions(+), 27 deletions(-) diff --git a/packages/client-sdk-nodejs/src/internal/grpc/storage-client-timeout-interceptor.ts b/packages/client-sdk-nodejs/src/internal/grpc/storage-client-timeout-interceptor.ts index e00a8bfa8..73c38974f 100644 --- a/packages/client-sdk-nodejs/src/internal/grpc/storage-client-timeout-interceptor.ts +++ b/packages/client-sdk-nodejs/src/internal/grpc/storage-client-timeout-interceptor.ts @@ -18,7 +18,8 @@ export function createStorageClientTimeoutInterceptor( export class StorageClientTimeoutInterceptor { private readonly logger: MomentoLogger; private readonly responseDataReceivedTimeoutMs: number; - private readonly overallDeadline: number; + private readonly overallRequestTimeoutMs: number; + private overallDeadline?: Date; constructor( loggerFactory: MomentoLoggerFactory, @@ -27,41 +28,59 @@ export class StorageClientTimeoutInterceptor { ) { this.logger = loggerFactory.getLogger(this); this.responseDataReceivedTimeoutMs = responseDataReceivedTimeoutMs; - const newDate = new Date(Date.now()); - this.overallDeadline = newDate.setMilliseconds( - newDate.getMilliseconds() + overallRequestTimeoutMs - ); - this.logger.debug( - `creating interceptor, overall deadline: ${this.overallDeadline}` - ); + this.overallRequestTimeoutMs = overallRequestTimeoutMs; } public createTimeoutInterceptor(): Interceptor { return (options, nextCall) => { + // Replace default timeout with the desired overall timeout + // on the first time through and set the first incremental timeout + if (this.overallDeadline === undefined) { + const newDate = new Date(Date.now()); + newDate.setMilliseconds( + newDate.getMilliseconds() + this.overallRequestTimeoutMs + ); + this.overallDeadline = newDate; + + const deadline = new Date(Date.now()); + deadline.setMilliseconds( + deadline.getMilliseconds() + this.responseDataReceivedTimeoutMs + ); + options.deadline = deadline; + this.logger.debug(`new deadline set to ${options.deadline.valueOf()}`); + return new InterceptingCall(nextCall(options)); + } + + const receivedDeadline = options.deadline?.valueOf() || 0; this.logger.debug( - `intercepting call with options.deadline ${ - options.deadline?.valueOf() || 'undefined' - }` + `intercepting call with options.deadline ${receivedDeadline}` ); - const deadline = new Date(Date.now()); - deadline.setMilliseconds( - deadline.getMilliseconds() + this.responseDataReceivedTimeoutMs - ); + // Reset incremental deadline only if it has been reached + if (receivedDeadline < new Date(Date.now()).valueOf()) { + this.logger.debug('received deadline < current time, resetting'); - if (deadline.valueOf() > this.overallDeadline.valueOf()) { - this.logger.debug( - 'Unable to successfully retry request within client timeout, canceling request' - ); - const call = new InterceptingCall(nextCall(options)); - call.cancelWithStatus( - status.CANCELLED, - 'Unable to successfully retry request within client timeout' + const deadline = new Date(Date.now()); + deadline.setMilliseconds( + deadline.getMilliseconds() + this.responseDataReceivedTimeoutMs ); - return call; - } else { - options.deadline = deadline; - this.logger.debug(`new deadline set to ${options.deadline.valueOf()}`); + + if (deadline.valueOf() > this.overallDeadline.valueOf()) { + this.logger.debug( + 'Unable to successfully retry request within client timeout, canceling request' + ); + const call = new InterceptingCall(nextCall(options)); + call.cancelWithStatus( + status.CANCELLED, + 'Unable to successfully retry request within client timeout' + ); + return call; + } else { + options.deadline = deadline; + this.logger.debug( + `new deadline set to ${options.deadline.valueOf()}` + ); + } } return new InterceptingCall(nextCall(options)); From c11c892b01ae8ae6c642b7f753bf1ca22ff57ab8 Mon Sep 17 00:00:00 2001 From: anitarua Date: Mon, 5 Aug 2024 10:04:06 -0700 Subject: [PATCH 06/43] remove unnecessary comment --- .../src/config/retry/storage-default-eligibility-strategy.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/client-sdk-nodejs/src/config/retry/storage-default-eligibility-strategy.ts b/packages/client-sdk-nodejs/src/config/retry/storage-default-eligibility-strategy.ts index 646c7d31a..acd2e0083 100644 --- a/packages/client-sdk-nodejs/src/config/retry/storage-default-eligibility-strategy.ts +++ b/packages/client-sdk-nodejs/src/config/retry/storage-default-eligibility-strategy.ts @@ -28,7 +28,6 @@ const retryableGrpcStatusCodes: Array = [ // Status.UNAUTHENTICATED ]; -// TODO: verify correct strings. Should be proto file's "./" right? const retryableRequestTypes: Array = [ '/store.Store/Put', '/store.Store/Get', From 348e0b59224362118dfc18a78d03289292729db9 Mon Sep 17 00:00:00 2001 From: anitarua Date: Mon, 5 Aug 2024 10:04:26 -0700 Subject: [PATCH 07/43] no need to create new date in if statement --- .../src/internal/grpc/storage-client-timeout-interceptor.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/client-sdk-nodejs/src/internal/grpc/storage-client-timeout-interceptor.ts b/packages/client-sdk-nodejs/src/internal/grpc/storage-client-timeout-interceptor.ts index 73c38974f..0716a6dcd 100644 --- a/packages/client-sdk-nodejs/src/internal/grpc/storage-client-timeout-interceptor.ts +++ b/packages/client-sdk-nodejs/src/internal/grpc/storage-client-timeout-interceptor.ts @@ -57,7 +57,7 @@ export class StorageClientTimeoutInterceptor { ); // Reset incremental deadline only if it has been reached - if (receivedDeadline < new Date(Date.now()).valueOf()) { + if (receivedDeadline < Date.now()) { this.logger.debug('received deadline < current time, resetting'); const deadline = new Date(Date.now()); From b93b415e9370715d478dd3a9c872a27d35dee2c7 Mon Sep 17 00:00:00 2001 From: anitarua Date: Mon, 5 Aug 2024 10:13:00 -0700 Subject: [PATCH 08/43] rename client timeout to request timeout --- .../src/config/storage-configuration.ts | 8 ++++---- .../config/transport/storage/transport-strategy.ts | 12 ++++++------ 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/client-sdk-nodejs/src/config/storage-configuration.ts b/packages/client-sdk-nodejs/src/config/storage-configuration.ts index 624faa2e7..9614d913a 100644 --- a/packages/client-sdk-nodejs/src/config/storage-configuration.ts +++ b/packages/client-sdk-nodejs/src/config/storage-configuration.ts @@ -28,10 +28,10 @@ export interface StorageConfiguration { /** * Convenience copy constructor that updates the client-side timeout setting in the TransportStrategy - * @param {number} clientTimeoutMillis + * @param {number} requestTimeoutMillis * @returns {StorageConfiguration} a new Configuration object with its TransportStrategy updated to use the specified client timeout */ - withClientTimeoutMillis(clientTimeoutMillis: number): StorageConfiguration; + withRequestTimeoutMillis(requestTimeoutMillis: number): StorageConfiguration; /** * Copy constructor for overriding TransportStrategy @@ -88,11 +88,11 @@ export class StorageClientConfiguration implements StorageConfiguration { return this.retryStrategy; } - withClientTimeoutMillis(clientTimeoutMillis: number): StorageConfiguration { + withRequestTimeoutMillis(requestTimeoutMillis: number): StorageConfiguration { return new StorageClientConfiguration({ loggerFactory: this.loggerFactory, transportStrategy: - this.transportStrategy.withClientTimeoutMillis(clientTimeoutMillis), + this.transportStrategy.withRequestTimeoutMillis(requestTimeoutMillis), retryStrategy: this.retryStrategy, }); } diff --git a/packages/client-sdk-nodejs/src/config/transport/storage/transport-strategy.ts b/packages/client-sdk-nodejs/src/config/transport/storage/transport-strategy.ts index 8ebe34799..8498104cb 100644 --- a/packages/client-sdk-nodejs/src/config/transport/storage/transport-strategy.ts +++ b/packages/client-sdk-nodejs/src/config/transport/storage/transport-strategy.ts @@ -28,11 +28,11 @@ export interface StorageTransportStrategy { /** * Copy constructor to update the client-side timeout - * @param {number} clientTimeoutMillis + * @param {number} requestTimeoutMillis * @returns {StorageTransportStrategy} a new StorageTransportStrategy with the specified client timeout */ - withClientTimeoutMillis( - clientTimeoutMillis: number + withRequestTimeoutMillis( + requestTimeoutMillis: number ): StorageTransportStrategy; /** @@ -105,12 +105,12 @@ export class StaticStorageTransportStrategy }); } - withClientTimeoutMillis( - clientTimeoutMillis: number + withRequestTimeoutMillis( + requestTimeoutMillis: number ): StorageTransportStrategy { return new StaticStorageTransportStrategy({ grpcConfiguration: - this.grpcConfig.withDeadlineMillis(clientTimeoutMillis), + this.grpcConfig.withDeadlineMillis(requestTimeoutMillis), responseDataReceivedTimeout: this.responseDataReceivedTimeout, }); } From 6eab3800b33e7aa6114ec317367b04ca1dc3db55 Mon Sep 17 00:00:00 2001 From: anitarua Date: Mon, 5 Aug 2024 10:23:40 -0700 Subject: [PATCH 09/43] move response data received timeout to grpc config level and add millis suffix --- .../src/config/storage-configurations.ts | 1 + .../transport/storage/grpc-configuration.ts | 18 +++++++ .../transport/storage/transport-strategy.ts | 47 +++++++++---------- .../src/internal/storage-data-client.ts | 3 +- .../test/unit/storage-retry.test.ts | 6 +-- 5 files changed, 47 insertions(+), 28 deletions(-) diff --git a/packages/client-sdk-nodejs/src/config/storage-configurations.ts b/packages/client-sdk-nodejs/src/config/storage-configurations.ts index 458ae5989..4af02ec3d 100644 --- a/packages/client-sdk-nodejs/src/config/storage-configurations.ts +++ b/packages/client-sdk-nodejs/src/config/storage-configurations.ts @@ -42,6 +42,7 @@ export class Laptop extends StorageClientConfiguration { transportStrategy: new StaticStorageTransportStrategy({ grpcConfiguration: new StaticStorageGrpcConfiguration({ deadlineMillis: 5000, + responseDataReceivedTimeoutMillis: 1000, }), }), retryStrategy: defaultRetryStrategy(loggerFactory), diff --git a/packages/client-sdk-nodejs/src/config/transport/storage/grpc-configuration.ts b/packages/client-sdk-nodejs/src/config/transport/storage/grpc-configuration.ts index 67ac8b455..8dde06bef 100644 --- a/packages/client-sdk-nodejs/src/config/transport/storage/grpc-configuration.ts +++ b/packages/client-sdk-nodejs/src/config/transport/storage/grpc-configuration.ts @@ -4,6 +4,10 @@ export interface StorageGrpcConfigurationProps { * with a DeadlineExceeded error. */ deadlineMillis: number; + /** + * number of milliseconds the client is willing to wait for response data to be received before retrying (defaults to 1000ms). After the deadlineMillis has been reached, the client will terminate the RPC with a Cancelled error. + */ + responseDataReceivedTimeoutMillis: number; } /** @@ -24,4 +28,18 @@ export interface StorageGrpcConfiguration { * @returns {StorageGrpcConfiguration} a new StorageGrpcConfiguration with the specified client-side deadline */ withDeadlineMillis(deadlineMillis: number): StorageGrpcConfiguration; + + /** + * @returns {number} number of milliseconds the client is willing to wait for response data to be received before retrying (defaults to 1000ms). After the deadlineMillis has been reached, the client will terminate the RPC with a Cancelled error. + */ + getResponseDataReceivedTimeoutMillis(): number; + + /** + * Copy constructor for overriding the client-side deadline for receiving response data before retrying the request + * @param {number} responseDataReceivedTimeoutMillis + * @returns {StorageGrpcConfiguration} a new StorageGrpcConfiguration with the specified timeout + */ + withResponseDataReceivedTimeoutMillis( + responseDataReceivedTimeoutMillis: number + ): StorageGrpcConfiguration; } diff --git a/packages/client-sdk-nodejs/src/config/transport/storage/transport-strategy.ts b/packages/client-sdk-nodejs/src/config/transport/storage/transport-strategy.ts index 8498104cb..008f4e8aa 100644 --- a/packages/client-sdk-nodejs/src/config/transport/storage/transport-strategy.ts +++ b/packages/client-sdk-nodejs/src/config/transport/storage/transport-strategy.ts @@ -11,12 +11,6 @@ export interface StorageTransportStrategy { */ getGrpcConfig(): StorageGrpcConfiguration; - /** - * Configures time to wait for response data to be received before retrying (defaults to 1s). - * @returns {StorageGrpcConfiguration} - */ - getResponseDataReceivedTimeout(): number; - /** * Copy constructor for overriding the gRPC configuration * @param {TopicGrpcConfiguration} grpcConfig @@ -37,11 +31,11 @@ export interface StorageTransportStrategy { /** * Copy constructor to update the ResponseDataReceivedTimeout - * @param {number} responseDataReceivedTimeout + * @param {number} responseDataReceivedTimeoutMillis * @returns {StorageTransportStrategy} a new StorageTransportStrategy with the specified client timeout */ withResponseDataReceivedTimeout( - responseDataReceivedTimeout: number + responseDataReceivedTimeoutMillis: number ): StorageTransportStrategy; } @@ -50,19 +44,18 @@ export interface StorageTransportStrategyProps { * low-level gRPC settings for communication with the Momento server */ grpcConfiguration: StorageGrpcConfiguration; - /** - * time to wait for response data to be received before retrying (defaults to 1s) - */ - responseDataReceivedTimeout?: number; } export class StaticStorageGrpcConfiguration implements StorageGrpcConfiguration { private readonly deadlineMillis: number; + private readonly responseDataReceivedTimeoutMillis: number; constructor(props: StorageGrpcConfigurationProps) { this.deadlineMillis = props.deadlineMillis; + this.responseDataReceivedTimeoutMillis = + props.responseDataReceivedTimeoutMillis; } getDeadlineMillis(): number { @@ -72,6 +65,20 @@ export class StaticStorageGrpcConfiguration withDeadlineMillis(deadlineMillis: number): StorageGrpcConfiguration { return new StaticStorageGrpcConfiguration({ deadlineMillis: deadlineMillis, + responseDataReceivedTimeoutMillis: this.responseDataReceivedTimeoutMillis, + }); + } + + getResponseDataReceivedTimeoutMillis(): number { + return this.responseDataReceivedTimeoutMillis; + } + + withResponseDataReceivedTimeoutMillis( + responseDataReceivedTimeoutMillis: number + ): StorageGrpcConfiguration { + return new StaticStorageGrpcConfiguration({ + deadlineMillis: this.deadlineMillis, + responseDataReceivedTimeoutMillis: responseDataReceivedTimeoutMillis, }); } } @@ -80,28 +87,20 @@ export class StaticStorageTransportStrategy implements StorageTransportStrategy { private readonly grpcConfig: StorageGrpcConfiguration; - private readonly responseDataReceivedTimeout: number; constructor(props: StorageTransportStrategyProps) { this.grpcConfig = props.grpcConfiguration; - this.responseDataReceivedTimeout = - props.responseDataReceivedTimeout ?? 1000; } getGrpcConfig(): StorageGrpcConfiguration { return this.grpcConfig; } - getResponseDataReceivedTimeout(): number { - return this.responseDataReceivedTimeout; - } - withGrpcConfig( grpcConfig: StorageGrpcConfiguration ): StorageTransportStrategy { return new StaticStorageTransportStrategy({ grpcConfiguration: grpcConfig, - responseDataReceivedTimeout: this.responseDataReceivedTimeout, }); } @@ -111,16 +110,16 @@ export class StaticStorageTransportStrategy return new StaticStorageTransportStrategy({ grpcConfiguration: this.grpcConfig.withDeadlineMillis(requestTimeoutMillis), - responseDataReceivedTimeout: this.responseDataReceivedTimeout, }); } withResponseDataReceivedTimeout( - responseDataReceivedTimeout: number + responseDataReceivedTimeoutMillis: number ): StorageTransportStrategy { return new StaticStorageTransportStrategy({ - grpcConfiguration: this.grpcConfig, - responseDataReceivedTimeout: responseDataReceivedTimeout, + grpcConfiguration: this.grpcConfig.withResponseDataReceivedTimeoutMillis( + responseDataReceivedTimeoutMillis + ), }); } } diff --git a/packages/client-sdk-nodejs/src/internal/storage-data-client.ts b/packages/client-sdk-nodejs/src/internal/storage-data-client.ts index 1e8af82a6..11ddc4659 100644 --- a/packages/client-sdk-nodejs/src/internal/storage-data-client.ts +++ b/packages/client-sdk-nodejs/src/internal/storage-data-client.ts @@ -54,7 +54,8 @@ export class StorageDataClient implements IStorageDataClient { .getDeadlineMillis(); this.responseDataReceivedTimeoutMs = this.configuration .getTransportStrategy() - .getResponseDataReceivedTimeout(); + .getGrpcConfig() + .getResponseDataReceivedTimeoutMillis(); this.validateRequestTimeout(this.requestTimeoutMs); this.logger.debug( `Creating leaderboard client using endpoint: '${this.credentialProvider.getStorageEndpoint()}'` diff --git a/packages/client-sdk-nodejs/test/unit/storage-retry.test.ts b/packages/client-sdk-nodejs/test/unit/storage-retry.test.ts index 380aac5ba..7d43aeb96 100644 --- a/packages/client-sdk-nodejs/test/unit/storage-retry.test.ts +++ b/packages/client-sdk-nodejs/test/unit/storage-retry.test.ts @@ -18,10 +18,10 @@ describe('storage configuration', () => { const testLoggerFactory = new DefaultMomentoLoggerFactory(); const testGrpcConfiguration = new StaticStorageGrpcConfiguration({ deadlineMillis: 5000, + responseDataReceivedTimeoutMillis: 1000, }); const testTransportStrategy = new StaticStorageTransportStrategy({ grpcConfiguration: testGrpcConfiguration, - responseDataReceivedTimeout: 1000, }); const testRetryStrategy = new DefaultStorageRetryStrategy({ loggerFactory: testLoggerFactory, @@ -52,8 +52,8 @@ describe('storage configuration', () => { it('should support overriding transport strategy', () => { const newTransportStrategy = new StaticStorageTransportStrategy({ - grpcConfiguration: testGrpcConfiguration, - responseDataReceivedTimeout: 5000, + grpcConfiguration: + testGrpcConfiguration.withResponseDataReceivedTimeoutMillis(5000), }); const configWithNewTransportStrategy = testConfiguration.withTransportStrategy(newTransportStrategy); From cbdfff39ac60fe64c6a7c525f5ae3e803ad28d03 Mon Sep 17 00:00:00 2001 From: anitarua Date: Wed, 7 Aug 2024 16:23:00 -0700 Subject: [PATCH 10/43] add initial implementation of checking metadata for retries --- .../storage-default-eligibility-strategy.ts | 41 +++++++++++++++++-- 1 file changed, 38 insertions(+), 3 deletions(-) diff --git a/packages/client-sdk-nodejs/src/config/retry/storage-default-eligibility-strategy.ts b/packages/client-sdk-nodejs/src/config/retry/storage-default-eligibility-strategy.ts index acd2e0083..ba6dcd736 100644 --- a/packages/client-sdk-nodejs/src/config/retry/storage-default-eligibility-strategy.ts +++ b/packages/client-sdk-nodejs/src/config/retry/storage-default-eligibility-strategy.ts @@ -52,10 +52,45 @@ export class DefaultStorageEligibilityStrategy implements EligibilityStrategy { const errorMetadata = props.grpcStatus.metadata.get('err'); // Expecting only one error condition in metadata if (errorMetadata.length === 1) { - // TODO: check error metadata for specific conditions that should not be retried - if (errorMetadata[0] === 'unacceptable retry condition') return false; + switch (errorMetadata[0].toString()) { + case 'momento_general_err': + return false; + case 'server_is_busy': { + // Retry disposition will show up only for err metadata "server_is_busy" + const retryMetadata = + props.grpcStatus.metadata.get('retry_disposition'); + if (retryMetadata.length === 1) { + const retryDisposition = retryMetadata[0].toString(); + switch (retryMetadata[0].toString()) { + case 'retryable': + // Retryable = could retry soon (default 100ms) + return true; + case 'possibly_retryable': + // Possibly retryable = could retry in a second or two + return true; // return a value here instead? + default: + this.logger.debug( + `Unknown retry disposition value: ${retryDisposition}` + ); + return false; + } + } + return false; + } + case 'invalid_type': + return false; + case 'item_not_found': + return false; + case 'store_not_found': + return false; + default: + this.logger.debug( + `Unknown error metadata value: ${errorMetadata[0].toString()}` + ); + return false; + } } else { - // If no metadata, fall back to checking idemopotency + // If no metadata to check, fall back to checking RPC idemopotency if (!retryableRequestTypes.includes(props.grpcRequest.path)) { this.logger.debug( `Request with type ${props.grpcRequest.path} is not retryable.` From a271ebe5f6fe53a7dfe294017b08f45584beedeb Mon Sep 17 00:00:00 2001 From: anitarua Date: Tue, 13 Aug 2024 11:43:32 -0700 Subject: [PATCH 11/43] revise retry metadata parsing --- .../storage-default-eligibility-strategy.ts | 73 +++++++------------ 1 file changed, 25 insertions(+), 48 deletions(-) diff --git a/packages/client-sdk-nodejs/src/config/retry/storage-default-eligibility-strategy.ts b/packages/client-sdk-nodejs/src/config/retry/storage-default-eligibility-strategy.ts index ba6dcd736..8578d4e31 100644 --- a/packages/client-sdk-nodejs/src/config/retry/storage-default-eligibility-strategy.ts +++ b/packages/client-sdk-nodejs/src/config/retry/storage-default-eligibility-strategy.ts @@ -4,6 +4,7 @@ import { EligibilityStrategy, EligibleForRetryProps, } from './eligibility-strategy'; +import {Metadata} from '@grpc/grpc-js'; const retryableGrpcStatusCodes: Array = [ // including all the status codes for reference, but @@ -49,56 +50,32 @@ export class DefaultStorageEligibilityStrategy implements EligibilityStrategy { return false; } - const errorMetadata = props.grpcStatus.metadata.get('err'); - // Expecting only one error condition in metadata - if (errorMetadata.length === 1) { - switch (errorMetadata[0].toString()) { - case 'momento_general_err': - return false; - case 'server_is_busy': { - // Retry disposition will show up only for err metadata "server_is_busy" - const retryMetadata = - props.grpcStatus.metadata.get('retry_disposition'); - if (retryMetadata.length === 1) { - const retryDisposition = retryMetadata[0].toString(); - switch (retryMetadata[0].toString()) { - case 'retryable': - // Retryable = could retry soon (default 100ms) - return true; - case 'possibly_retryable': - // Possibly retryable = could retry in a second or two - return true; // return a value here instead? - default: - this.logger.debug( - `Unknown retry disposition value: ${retryDisposition}` - ); - return false; - } - } - return false; - } - case 'invalid_type': - return false; - case 'item_not_found': - return false; - case 'store_not_found': - return false; - default: - this.logger.debug( - `Unknown error metadata value: ${errorMetadata[0].toString()}` - ); - return false; - } - } else { - // If no metadata to check, fall back to checking RPC idemopotency - if (!retryableRequestTypes.includes(props.grpcRequest.path)) { - this.logger.debug( - `Request with type ${props.grpcRequest.path} is not retryable.` - ); - return false; - } + // If retry disposition metadata is available and the value is "retryable", + // it is safe to retry regardless of idempotency. + const retryMetadata = this.getRetryDispositionMetadata( + props.grpcStatus.metadata + ); + if (retryMetadata === 'retryable') { + return true; + } + + // Otherwise, if there is no retry metadata or the retry disposition is + // "possibly_retryable", it is safe to retry only idempotent commands. + if (!retryableRequestTypes.includes(props.grpcRequest.path)) { + this.logger.debug( + `Request with type ${props.grpcRequest.path} is not retryable.` + ); + return false; } return true; } + + private getRetryDispositionMetadata(metadata: Metadata): string | undefined { + const retryMetadata = metadata.get('retry_disposition'); + if (retryMetadata.length === 1) { + return retryMetadata[0].toString(); + } + return undefined; + } } From 02b7d9348a8474cfabf19360e290d5d7879ce123 Mon Sep 17 00:00:00 2001 From: anitarua Date: Wed, 14 Aug 2024 14:23:04 -0700 Subject: [PATCH 12/43] fix auth header bug and pass request metadata to interceptors --- .../src/config/retry/eligibility-strategy.ts | 3 ++- .../client-sdk-nodejs/src/config/retry/retry-strategy.ts | 3 ++- .../src/internal/grpc/headers-interceptor.ts | 6 +++--- .../src/internal/grpc/retry-interceptor.ts | 1 + 4 files changed, 8 insertions(+), 5 deletions(-) diff --git a/packages/client-sdk-nodejs/src/config/retry/eligibility-strategy.ts b/packages/client-sdk-nodejs/src/config/retry/eligibility-strategy.ts index 600d72106..20e41e456 100644 --- a/packages/client-sdk-nodejs/src/config/retry/eligibility-strategy.ts +++ b/packages/client-sdk-nodejs/src/config/retry/eligibility-strategy.ts @@ -1,9 +1,10 @@ -import {StatusObject} from '@grpc/grpc-js'; +import {Metadata, StatusObject} from '@grpc/grpc-js'; import {ClientMethodDefinition} from '@grpc/grpc-js/build/src/make-client'; export interface EligibleForRetryProps { grpcStatus: StatusObject; grpcRequest: ClientMethodDefinition; + requestMetadata?: Metadata; } export interface EligibilityStrategy { diff --git a/packages/client-sdk-nodejs/src/config/retry/retry-strategy.ts b/packages/client-sdk-nodejs/src/config/retry/retry-strategy.ts index 4a9a9814d..e5c35518b 100644 --- a/packages/client-sdk-nodejs/src/config/retry/retry-strategy.ts +++ b/packages/client-sdk-nodejs/src/config/retry/retry-strategy.ts @@ -1,10 +1,11 @@ -import {StatusObject} from '@grpc/grpc-js'; +import {Metadata, StatusObject} from '@grpc/grpc-js'; import {ClientMethodDefinition} from '@grpc/grpc-js/build/src/make-client'; export interface DeterminewhenToRetryRequestProps { grpcStatus: StatusObject; grpcRequest: ClientMethodDefinition; attemptNumber: number; + requestMetadata?: Metadata; } export interface RetryStrategy { diff --git a/packages/client-sdk-nodejs/src/internal/grpc/headers-interceptor.ts b/packages/client-sdk-nodejs/src/internal/grpc/headers-interceptor.ts index ccbdbb097..2658ed245 100644 --- a/packages/client-sdk-nodejs/src/internal/grpc/headers-interceptor.ts +++ b/packages/client-sdk-nodejs/src/internal/grpc/headers-interceptor.ts @@ -36,9 +36,9 @@ export class HeaderInterceptorProvider { return (options, nextCall) => { return new InterceptingCall(nextCall(options), { start: (metadata, listener, next) => { - this.headersToAddEveryTime.forEach(h => - metadata.add(h.name, h.value) - ); + this.headersToAddEveryTime.forEach(h => { + metadata.set(h.name, h.value); + }); if (!this.areOnlyOnceHeadersSent) { this.areOnlyOnceHeadersSent = true; this.headersToAddOnce.forEach(h => metadata.add(h.name, h.value)); diff --git a/packages/client-sdk-nodejs/src/internal/grpc/retry-interceptor.ts b/packages/client-sdk-nodejs/src/internal/grpc/retry-interceptor.ts index 2f62d3ceb..47315a94f 100644 --- a/packages/client-sdk-nodejs/src/internal/grpc/retry-interceptor.ts +++ b/packages/client-sdk-nodejs/src/internal/grpc/retry-interceptor.ts @@ -82,6 +82,7 @@ export class RetryInterceptor { grpcStatus: status, grpcRequest: options.method_definition, attemptNumber: attempts, + requestMetadata: metadata, }); if (whenToRetry === null) { From 9c278c449e0d550947cdf39e8409b0252725f23d Mon Sep 17 00:00:00 2001 From: anitarua Date: Mon, 19 Aug 2024 16:23:54 -0700 Subject: [PATCH 13/43] add jitter to retry interceptor --- .../src/internal/grpc/retry-interceptor.ts | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/packages/client-sdk-nodejs/src/internal/grpc/retry-interceptor.ts b/packages/client-sdk-nodejs/src/internal/grpc/retry-interceptor.ts index 47315a94f..765f6b751 100644 --- a/packages/client-sdk-nodejs/src/internal/grpc/retry-interceptor.ts +++ b/packages/client-sdk-nodejs/src/internal/grpc/retry-interceptor.ts @@ -25,6 +25,10 @@ export function createRetryInterceptorIfEnabled( ]; } +function addJitter(whenToRetry: number): number { + return Math.random() * (1.2 * whenToRetry) - 1.1 * whenToRetry; +} + export class RetryInterceptor { private readonly logger: MomentoLogger; private readonly retryStrategy: RetryStrategy; @@ -92,10 +96,14 @@ export class RetryInterceptor { savedMessageNext(savedReceiveMessage); next(status); } else { + const whenToRetryWithJitter = addJitter(whenToRetry); logger.debug( - `Request eligible for retry: path: ${options.method_definition.path}; response status code: ${status.code}; number of attempts (${attempts}); will retry in ${whenToRetry}ms` + `Request eligible for retry: path: ${options.method_definition.path}; response status code: ${status.code}; number of attempts (${attempts}); will retry in ${whenToRetryWithJitter}ms` + ); + setTimeout( + () => retry(message, metadata), + whenToRetryWithJitter ); - setTimeout(() => retry(message, metadata), whenToRetry); } }, }); @@ -119,12 +127,13 @@ export class RetryInterceptor { savedMessageNext(savedReceiveMessage); next(status); } else { + const whenToRetryWithJitter = addJitter(whenToRetry); logger.debug( - `Request eligible for retry: path: ${options.method_definition.path}; response status code: ${status.code}; number of attempts (${attempts}); will retry in ${whenToRetry}ms` + `Request eligible for retry: path: ${options.method_definition.path}; response status code: ${status.code}; number of attempts (${attempts}); will retry in ${whenToRetryWithJitter}ms` ); setTimeout( () => retry(savedSendMessage, savedMetadata), - whenToRetry + whenToRetryWithJitter ); } } From 2dc26d058e088b9752ef64f7353213465a4e201f Mon Sep 17 00:00:00 2001 From: anitarua Date: Tue, 20 Aug 2024 11:40:11 -0700 Subject: [PATCH 14/43] fix: jitter created negative millis --- .../client-sdk-nodejs/src/internal/grpc/retry-interceptor.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/client-sdk-nodejs/src/internal/grpc/retry-interceptor.ts b/packages/client-sdk-nodejs/src/internal/grpc/retry-interceptor.ts index 765f6b751..aa5b2feab 100644 --- a/packages/client-sdk-nodejs/src/internal/grpc/retry-interceptor.ts +++ b/packages/client-sdk-nodejs/src/internal/grpc/retry-interceptor.ts @@ -26,7 +26,7 @@ export function createRetryInterceptorIfEnabled( } function addJitter(whenToRetry: number): number { - return Math.random() * (1.2 * whenToRetry) - 1.1 * whenToRetry; + return (1 + Math.random()) * (1.2 * whenToRetry) - 1.1 * whenToRetry; } export class RetryInterceptor { From b0be6742c7fb8d2a02544cc3247f43070319dafa Mon Sep 17 00:00:00 2001 From: anitarua Date: Tue, 20 Aug 2024 11:41:06 -0700 Subject: [PATCH 15/43] remove data client interceptor piping, restructure response data timeout millis interface exposure --- .../retry/storage-default-retry-strategy.ts | 20 ++++++++-- .../src/config/storage-configurations.ts | 1 - .../transport/storage/grpc-configuration.ts | 18 --------- .../transport/storage/transport-strategy.ts | 36 ----------------- .../src/internal/storage-data-client.ts | 40 ++++++------------- .../test/unit/storage-retry.test.ts | 4 +- 6 files changed, 30 insertions(+), 89 deletions(-) diff --git a/packages/client-sdk-nodejs/src/config/retry/storage-default-retry-strategy.ts b/packages/client-sdk-nodejs/src/config/retry/storage-default-retry-strategy.ts index 3f703ea51..33c7376a5 100644 --- a/packages/client-sdk-nodejs/src/config/retry/storage-default-retry-strategy.ts +++ b/packages/client-sdk-nodejs/src/config/retry/storage-default-retry-strategy.ts @@ -9,21 +9,28 @@ import {DefaultStorageEligibilityStrategy} from './storage-default-eligibility-s export interface DefaultStorageRetryStrategyProps { loggerFactory: MomentoLoggerFactory; eligibilityStrategy?: EligibilityStrategy; + // Retry request after a fixed time interval (defaults to 100ms) - retryDelayInterval?: number; + retryDelayIntervalMillis?: number; + + // Number of milliseconds the client is willing to wait for response data to be received before retrying (defaults to 1000ms). After the overarching GRPC config deadlineMillis has been reached, the client will terminate the RPC with a Cancelled error. + responseDataReceivedTimeoutMillis?: number; } export class DefaultStorageRetryStrategy implements RetryStrategy { private readonly logger: MomentoLogger; private readonly eligibilityStrategy: EligibilityStrategy; - private readonly retryDelayInterval: number; + private readonly retryDelayIntervalMillis: number; + private readonly responseDataReceivedTimeoutMillis: number; constructor(props: DefaultStorageRetryStrategyProps) { this.logger = props.loggerFactory.getLogger(this); this.eligibilityStrategy = props.eligibilityStrategy ?? new DefaultStorageEligibilityStrategy(props.loggerFactory); - this.retryDelayInterval = props.retryDelayInterval ?? 100; + this.retryDelayIntervalMillis = props.retryDelayIntervalMillis ?? 100; + this.responseDataReceivedTimeoutMillis = + props.responseDataReceivedTimeoutMillis ?? 1000; } determineWhenToRetryRequest( @@ -36,10 +43,15 @@ export class DefaultStorageRetryStrategy implements RetryStrategy { // null means do not retry return null; } + this.logger.debug( `Request is eligible for retry (attempt ${props.attemptNumber}), retrying soon.` ); // retry after a fixed time interval has passed - return this.retryDelayInterval; + return this.retryDelayIntervalMillis; + } + + public getResponseDataReceivedTimeoutMillis(): number { + return this.responseDataReceivedTimeoutMillis; } } diff --git a/packages/client-sdk-nodejs/src/config/storage-configurations.ts b/packages/client-sdk-nodejs/src/config/storage-configurations.ts index 4af02ec3d..458ae5989 100644 --- a/packages/client-sdk-nodejs/src/config/storage-configurations.ts +++ b/packages/client-sdk-nodejs/src/config/storage-configurations.ts @@ -42,7 +42,6 @@ export class Laptop extends StorageClientConfiguration { transportStrategy: new StaticStorageTransportStrategy({ grpcConfiguration: new StaticStorageGrpcConfiguration({ deadlineMillis: 5000, - responseDataReceivedTimeoutMillis: 1000, }), }), retryStrategy: defaultRetryStrategy(loggerFactory), diff --git a/packages/client-sdk-nodejs/src/config/transport/storage/grpc-configuration.ts b/packages/client-sdk-nodejs/src/config/transport/storage/grpc-configuration.ts index 8dde06bef..67ac8b455 100644 --- a/packages/client-sdk-nodejs/src/config/transport/storage/grpc-configuration.ts +++ b/packages/client-sdk-nodejs/src/config/transport/storage/grpc-configuration.ts @@ -4,10 +4,6 @@ export interface StorageGrpcConfigurationProps { * with a DeadlineExceeded error. */ deadlineMillis: number; - /** - * number of milliseconds the client is willing to wait for response data to be received before retrying (defaults to 1000ms). After the deadlineMillis has been reached, the client will terminate the RPC with a Cancelled error. - */ - responseDataReceivedTimeoutMillis: number; } /** @@ -28,18 +24,4 @@ export interface StorageGrpcConfiguration { * @returns {StorageGrpcConfiguration} a new StorageGrpcConfiguration with the specified client-side deadline */ withDeadlineMillis(deadlineMillis: number): StorageGrpcConfiguration; - - /** - * @returns {number} number of milliseconds the client is willing to wait for response data to be received before retrying (defaults to 1000ms). After the deadlineMillis has been reached, the client will terminate the RPC with a Cancelled error. - */ - getResponseDataReceivedTimeoutMillis(): number; - - /** - * Copy constructor for overriding the client-side deadline for receiving response data before retrying the request - * @param {number} responseDataReceivedTimeoutMillis - * @returns {StorageGrpcConfiguration} a new StorageGrpcConfiguration with the specified timeout - */ - withResponseDataReceivedTimeoutMillis( - responseDataReceivedTimeoutMillis: number - ): StorageGrpcConfiguration; } diff --git a/packages/client-sdk-nodejs/src/config/transport/storage/transport-strategy.ts b/packages/client-sdk-nodejs/src/config/transport/storage/transport-strategy.ts index 008f4e8aa..126071776 100644 --- a/packages/client-sdk-nodejs/src/config/transport/storage/transport-strategy.ts +++ b/packages/client-sdk-nodejs/src/config/transport/storage/transport-strategy.ts @@ -28,15 +28,6 @@ export interface StorageTransportStrategy { withRequestTimeoutMillis( requestTimeoutMillis: number ): StorageTransportStrategy; - - /** - * Copy constructor to update the ResponseDataReceivedTimeout - * @param {number} responseDataReceivedTimeoutMillis - * @returns {StorageTransportStrategy} a new StorageTransportStrategy with the specified client timeout - */ - withResponseDataReceivedTimeout( - responseDataReceivedTimeoutMillis: number - ): StorageTransportStrategy; } export interface StorageTransportStrategyProps { @@ -50,12 +41,9 @@ export class StaticStorageGrpcConfiguration implements StorageGrpcConfiguration { private readonly deadlineMillis: number; - private readonly responseDataReceivedTimeoutMillis: number; constructor(props: StorageGrpcConfigurationProps) { this.deadlineMillis = props.deadlineMillis; - this.responseDataReceivedTimeoutMillis = - props.responseDataReceivedTimeoutMillis; } getDeadlineMillis(): number { @@ -65,20 +53,6 @@ export class StaticStorageGrpcConfiguration withDeadlineMillis(deadlineMillis: number): StorageGrpcConfiguration { return new StaticStorageGrpcConfiguration({ deadlineMillis: deadlineMillis, - responseDataReceivedTimeoutMillis: this.responseDataReceivedTimeoutMillis, - }); - } - - getResponseDataReceivedTimeoutMillis(): number { - return this.responseDataReceivedTimeoutMillis; - } - - withResponseDataReceivedTimeoutMillis( - responseDataReceivedTimeoutMillis: number - ): StorageGrpcConfiguration { - return new StaticStorageGrpcConfiguration({ - deadlineMillis: this.deadlineMillis, - responseDataReceivedTimeoutMillis: responseDataReceivedTimeoutMillis, }); } } @@ -112,14 +86,4 @@ export class StaticStorageTransportStrategy this.grpcConfig.withDeadlineMillis(requestTimeoutMillis), }); } - - withResponseDataReceivedTimeout( - responseDataReceivedTimeoutMillis: number - ): StorageTransportStrategy { - return new StaticStorageTransportStrategy({ - grpcConfiguration: this.grpcConfig.withResponseDataReceivedTimeoutMillis( - responseDataReceivedTimeoutMillis - ), - }); - } } diff --git a/packages/client-sdk-nodejs/src/internal/storage-data-client.ts b/packages/client-sdk-nodejs/src/internal/storage-data-client.ts index 11ddc4659..47747eb26 100644 --- a/packages/client-sdk-nodejs/src/internal/storage-data-client.ts +++ b/packages/client-sdk-nodejs/src/internal/storage-data-client.ts @@ -12,7 +12,6 @@ import { import {validateStoreName} from '@gomomento/sdk-core/dist/src/internal/utils'; import {store} from '@gomomento/generated-types/dist/store'; import {Header, HeaderInterceptorProvider} from './grpc/headers-interceptor'; -import {ClientTimeoutInterceptor} from './grpc/client-timeout-interceptor'; import { ChannelCredentials, Interceptor, @@ -28,6 +27,7 @@ import {StaticGrpcConfiguration} from '../config/transport/cache'; import {CacheServiceErrorMapper} from '../errors/cache-service-error-mapper'; import {createStorageClientTimeoutInterceptor} from './grpc/storage-client-timeout-interceptor'; import {createRetryInterceptorIfEnabled} from './grpc/retry-interceptor'; +import {DefaultStorageRetryStrategy} from '../config/retry/storage-default-retry-strategy'; export class StorageDataClient implements IStorageDataClient { private readonly configuration: StorageConfiguration; @@ -52,10 +52,9 @@ export class StorageDataClient implements IStorageDataClient { .getTransportStrategy() .getGrpcConfig() .getDeadlineMillis(); - this.responseDataReceivedTimeoutMs = this.configuration - .getTransportStrategy() - .getGrpcConfig() - .getResponseDataReceivedTimeoutMillis(); + this.responseDataReceivedTimeoutMs = ( + this.configuration.getRetryStrategy() as DefaultStorageRetryStrategy + ).getResponseDataReceivedTimeoutMillis(); this.validateRequestTimeout(this.requestTimeoutMs); this.logger.debug( `Creating leaderboard client using endpoint: '${this.credentialProvider.getStorageEndpoint()}'` @@ -124,7 +123,12 @@ export class StorageDataClient implements IStorageDataClient { this.configuration.getRetryStrategy() ), new HeaderInterceptorProvider(headers).createHeadersInterceptor(), - ClientTimeoutInterceptor(this.requestTimeoutMs), + // For this interceptor to work correctly, it must be specified last. + ...createStorageClientTimeoutInterceptor( + this.configuration.getLoggerFactory(), + this.requestTimeoutMs, + this.responseDataReceivedTimeoutMs + ), ]; } @@ -165,13 +169,7 @@ export class StorageDataClient implements IStorageDataClient { request, metadata, { - interceptors: this.interceptors.concat( - createStorageClientTimeoutInterceptor( - this.configuration.getLoggerFactory(), - this.requestTimeoutMs, - this.responseDataReceivedTimeoutMs - ) - ), + interceptors: this.interceptors, }, (err: ServiceError | null, resp) => { const value = resp?.value?.value; @@ -294,13 +292,7 @@ export class StorageDataClient implements IStorageDataClient { request, metadata, { - interceptors: this.interceptors.concat( - createStorageClientTimeoutInterceptor( - this.configuration.getLoggerFactory(), - this.requestTimeoutMs, - this.responseDataReceivedTimeoutMs - ) - ), + interceptors: this.interceptors, }, (err: ServiceError | null, resp) => { if (resp) { @@ -349,13 +341,7 @@ export class StorageDataClient implements IStorageDataClient { request, metadata, { - interceptors: this.interceptors.concat( - createStorageClientTimeoutInterceptor( - this.configuration.getLoggerFactory(), - this.requestTimeoutMs, - this.responseDataReceivedTimeoutMs - ) - ), + interceptors: this.interceptors, }, (err: ServiceError | null, resp) => { if (resp) { diff --git a/packages/client-sdk-nodejs/test/unit/storage-retry.test.ts b/packages/client-sdk-nodejs/test/unit/storage-retry.test.ts index 7d43aeb96..177692423 100644 --- a/packages/client-sdk-nodejs/test/unit/storage-retry.test.ts +++ b/packages/client-sdk-nodejs/test/unit/storage-retry.test.ts @@ -18,7 +18,6 @@ describe('storage configuration', () => { const testLoggerFactory = new DefaultMomentoLoggerFactory(); const testGrpcConfiguration = new StaticStorageGrpcConfiguration({ deadlineMillis: 5000, - responseDataReceivedTimeoutMillis: 1000, }); const testTransportStrategy = new StaticStorageTransportStrategy({ grpcConfiguration: testGrpcConfiguration, @@ -52,8 +51,7 @@ describe('storage configuration', () => { it('should support overriding transport strategy', () => { const newTransportStrategy = new StaticStorageTransportStrategy({ - grpcConfiguration: - testGrpcConfiguration.withResponseDataReceivedTimeoutMillis(5000), + grpcConfiguration: testGrpcConfiguration.withDeadlineMillis(10000), }); const configWithNewTransportStrategy = testConfiguration.withTransportStrategy(newTransportStrategy); From b40157beb5ba11cc584bf1c7ec81d039497b1e8d Mon Sep 17 00:00:00 2001 From: anitarua Date: Tue, 20 Aug 2024 11:55:13 -0700 Subject: [PATCH 16/43] add factory method for storage retry strategy --- .../retry/storage-default-retry-strategy.ts | 20 ++++++++++++++++++- .../test/unit/storage-retry.test.ts | 10 ++++++---- 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/packages/client-sdk-nodejs/src/config/retry/storage-default-retry-strategy.ts b/packages/client-sdk-nodejs/src/config/retry/storage-default-retry-strategy.ts index 33c7376a5..e1cffe0a0 100644 --- a/packages/client-sdk-nodejs/src/config/retry/storage-default-retry-strategy.ts +++ b/packages/client-sdk-nodejs/src/config/retry/storage-default-retry-strategy.ts @@ -3,7 +3,11 @@ import { RetryStrategy, } from './retry-strategy'; import {EligibilityStrategy} from './eligibility-strategy'; -import {MomentoLoggerFactory, MomentoLogger} from '../../'; +import { + MomentoLoggerFactory, + MomentoLogger, + DefaultMomentoLoggerFactory, +} from '../../'; import {DefaultStorageEligibilityStrategy} from './storage-default-eligibility-strategy'; export interface DefaultStorageRetryStrategyProps { @@ -55,3 +59,17 @@ export class DefaultStorageRetryStrategy implements RetryStrategy { return this.responseDataReceivedTimeoutMillis; } } + +export class DefaultStorageRetryStrategyFactory { + static getRetryStrategy( + props?: DefaultStorageRetryStrategyProps + ): DefaultStorageRetryStrategy { + return new DefaultStorageRetryStrategy({ + loggerFactory: props?.loggerFactory ?? new DefaultMomentoLoggerFactory(), + eligibilityStrategy: props?.eligibilityStrategy, + retryDelayIntervalMillis: props?.retryDelayIntervalMillis, + responseDataReceivedTimeoutMillis: + props?.responseDataReceivedTimeoutMillis, + }); + } +} diff --git a/packages/client-sdk-nodejs/test/unit/storage-retry.test.ts b/packages/client-sdk-nodejs/test/unit/storage-retry.test.ts index 177692423..23b014c2a 100644 --- a/packages/client-sdk-nodejs/test/unit/storage-retry.test.ts +++ b/packages/client-sdk-nodejs/test/unit/storage-retry.test.ts @@ -11,7 +11,7 @@ import { StaticStorageTransportStrategy, StorageClientConfiguration, } from '../../src'; -import {DefaultStorageRetryStrategy} from '../../src/config/retry/storage-default-retry-strategy'; +import {DefaultStorageRetryStrategyFactory} from '../../src/config/retry/storage-default-retry-strategy'; import {Metadata, StatusObject} from '@grpc/grpc-js'; describe('storage configuration', () => { @@ -22,9 +22,11 @@ describe('storage configuration', () => { const testTransportStrategy = new StaticStorageTransportStrategy({ grpcConfiguration: testGrpcConfiguration, }); - const testRetryStrategy = new DefaultStorageRetryStrategy({ - loggerFactory: testLoggerFactory, - }); + const testRetryStrategy = DefaultStorageRetryStrategyFactory.getRetryStrategy( + { + loggerFactory: testLoggerFactory, + } + ); const testConfiguration = new StorageClientConfiguration({ loggerFactory: testLoggerFactory, transportStrategy: testTransportStrategy, From 1768881f1f7143977cd29c69fc76efcf1d1ef026 Mon Sep 17 00:00:00 2001 From: anitarua Date: Tue, 20 Aug 2024 12:14:48 -0700 Subject: [PATCH 17/43] determine which interceptor to use based on retry strategy --- .../src/internal/storage-data-client.ts | 30 ++++++++++++------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/packages/client-sdk-nodejs/src/internal/storage-data-client.ts b/packages/client-sdk-nodejs/src/internal/storage-data-client.ts index 47747eb26..9c7c5ef33 100644 --- a/packages/client-sdk-nodejs/src/internal/storage-data-client.ts +++ b/packages/client-sdk-nodejs/src/internal/storage-data-client.ts @@ -28,6 +28,7 @@ import {CacheServiceErrorMapper} from '../errors/cache-service-error-mapper'; import {createStorageClientTimeoutInterceptor} from './grpc/storage-client-timeout-interceptor'; import {createRetryInterceptorIfEnabled} from './grpc/retry-interceptor'; import {DefaultStorageRetryStrategy} from '../config/retry/storage-default-retry-strategy'; +import {ClientTimeoutInterceptor} from './grpc/client-timeout-interceptor'; export class StorageDataClient implements IStorageDataClient { private readonly configuration: StorageConfiguration; @@ -38,7 +39,6 @@ export class StorageDataClient implements IStorageDataClient { private readonly client: store.StoreClient; private readonly interceptors: Interceptor[]; private static readonly DEFAULT_MAX_SESSION_MEMORY_MB: number = 256; - private readonly responseDataReceivedTimeoutMs: number; /** * @param {StorageClientPropsWithConfig} props @@ -52,9 +52,6 @@ export class StorageDataClient implements IStorageDataClient { .getTransportStrategy() .getGrpcConfig() .getDeadlineMillis(); - this.responseDataReceivedTimeoutMs = ( - this.configuration.getRetryStrategy() as DefaultStorageRetryStrategy - ).getResponseDataReceivedTimeoutMillis(); this.validateRequestTimeout(this.requestTimeoutMs); this.logger.debug( `Creating leaderboard client using endpoint: '${this.credentialProvider.getStorageEndpoint()}'` @@ -117,18 +114,31 @@ export class StorageDataClient implements IStorageDataClient { new Header('agent', `nodejs:store:${version}`), new Header('runtime-version', `nodejs:${process.versions.node}`), ]; + + // Determine which retry strategy is specified in the configuration + // and which interceptor to use. + const retryStrategy = this.configuration.getRetryStrategy(); + let timeoutInterceptor: Interceptor; + if (retryStrategy instanceof DefaultStorageRetryStrategy) { + const responseDataReceivedTimeoutMs = + retryStrategy.getResponseDataReceivedTimeoutMillis(); + timeoutInterceptor = createStorageClientTimeoutInterceptor( + _loggerFactory, + this.requestTimeoutMs, + responseDataReceivedTimeoutMs + )[0]; + } else { + timeoutInterceptor = ClientTimeoutInterceptor(this.requestTimeoutMs); + } + return [ ...createRetryInterceptorIfEnabled( this.configuration.getLoggerFactory(), this.configuration.getRetryStrategy() ), new HeaderInterceptorProvider(headers).createHeadersInterceptor(), - // For this interceptor to work correctly, it must be specified last. - ...createStorageClientTimeoutInterceptor( - this.configuration.getLoggerFactory(), - this.requestTimeoutMs, - this.responseDataReceivedTimeoutMs - ), + // For the timeout interceptors to work correctly, it must be specified last. + timeoutInterceptor, ]; } From ff84ac596d1c80d1b5b8c33c3c2fda1d88be5a22 Mon Sep 17 00:00:00 2001 From: anitarua Date: Tue, 20 Aug 2024 13:54:59 -0700 Subject: [PATCH 18/43] client interceptor determination using retry strategy --- .../grpc/client-timeout-interceptor.ts | 129 ++++++++++++++++-- .../storage-client-timeout-interceptor.ts | 89 ------------ .../src/internal/storage-data-client.ts | 24 +--- 3 files changed, 124 insertions(+), 118 deletions(-) delete mode 100644 packages/client-sdk-nodejs/src/internal/grpc/storage-client-timeout-interceptor.ts diff --git a/packages/client-sdk-nodejs/src/internal/grpc/client-timeout-interceptor.ts b/packages/client-sdk-nodejs/src/internal/grpc/client-timeout-interceptor.ts index 8060a3387..7183a1dac 100644 --- a/packages/client-sdk-nodejs/src/internal/grpc/client-timeout-interceptor.ts +++ b/packages/client-sdk-nodejs/src/internal/grpc/client-timeout-interceptor.ts @@ -1,14 +1,123 @@ -import {InterceptingCall, Interceptor} from '@grpc/grpc-js'; +import {MomentoLogger, MomentoLoggerFactory} from '@gomomento/sdk-core'; +import {InterceptingCall, Interceptor, status} from '@grpc/grpc-js'; +import {RetryStrategy} from '../../config/retry/retry-strategy'; +import {DefaultStorageRetryStrategy} from '../../config/retry/storage-default-retry-strategy'; +import {DefaultMomentoLoggerFactory} from '../../config/logging/default-momento-logger'; +// Determine which retry strategy is specified in the configuration +// and which interceptor to use. export const ClientTimeoutInterceptor = ( - requestTimeoutMs: number + overallRequestTimeoutMs: number, + retryStrategy?: RetryStrategy, + loggerFactory?: MomentoLoggerFactory ): Interceptor => { - return (options, nextCall) => { - if (!options.deadline) { - const deadline = new Date(Date.now()); - deadline.setMilliseconds(deadline.getMilliseconds() + requestTimeoutMs); - options.deadline = deadline; - } - return new InterceptingCall(nextCall(options)); - }; + if ( + retryStrategy !== undefined && + retryStrategy instanceof DefaultStorageRetryStrategy + ) { + const responseDataReceivedTimeoutMs = + retryStrategy.getResponseDataReceivedTimeoutMillis(); + return new RetryUntilTimeoutInterceptor( + loggerFactory ?? new DefaultMomentoLoggerFactory(), + overallRequestTimeoutMs, + responseDataReceivedTimeoutMs + ).createTimeoutInterceptor(); + } + return new BasicTimeoutInterceptor( + overallRequestTimeoutMs + ).createTimeoutInterceptor(); }; + +export class BasicTimeoutInterceptor { + private readonly overallRequestTimeoutMs: number; + + constructor(overallRequestTimeoutMs: number) { + this.overallRequestTimeoutMs = overallRequestTimeoutMs; + } + + public createTimeoutInterceptor(): Interceptor { + return (options, nextCall) => { + if (!options.deadline) { + const deadline = new Date(Date.now()); + deadline.setMilliseconds( + deadline.getMilliseconds() + this.overallRequestTimeoutMs + ); + options.deadline = deadline; + } + return new InterceptingCall(nextCall(options)); + }; + } +} + +export class RetryUntilTimeoutInterceptor { + private readonly logger: MomentoLogger; + private readonly responseDataReceivedTimeoutMs: number; + private readonly overallRequestTimeoutMs: number; + private overallDeadline?: Date; + + constructor( + loggerFactory: MomentoLoggerFactory, + overallRequestTimeoutMs: number, + responseDataReceivedTimeoutMs: number + ) { + this.logger = loggerFactory.getLogger(this); + this.responseDataReceivedTimeoutMs = responseDataReceivedTimeoutMs; + this.overallRequestTimeoutMs = overallRequestTimeoutMs; + } + + public createTimeoutInterceptor(): Interceptor { + return (options, nextCall) => { + // Replace default timeout with the desired overall timeout + // on the first time through and set the first incremental timeout + if (this.overallDeadline === undefined) { + const newDate = new Date(Date.now()); + newDate.setMilliseconds( + newDate.getMilliseconds() + this.overallRequestTimeoutMs + ); + this.overallDeadline = newDate; + + const deadline = new Date(Date.now()); + deadline.setMilliseconds( + deadline.getMilliseconds() + this.responseDataReceivedTimeoutMs + ); + options.deadline = deadline; + this.logger.debug(`new deadline set to ${options.deadline.valueOf()}`); + return new InterceptingCall(nextCall(options)); + } + + const receivedDeadline = options.deadline?.valueOf() || 0; + this.logger.debug( + `intercepting call with options.deadline ${receivedDeadline}` + ); + + // Reset incremental deadline only if it has been reached + if (receivedDeadline < Date.now()) { + this.logger.debug('received deadline < current time, resetting'); + + const deadline = new Date(Date.now()); + deadline.setMilliseconds( + deadline.getMilliseconds() + this.responseDataReceivedTimeoutMs + ); + + if (deadline.valueOf() > this.overallDeadline.valueOf()) { + this.logger.debug( + 'Unable to successfully retry request within client timeout, canceling request' + ); + const call = new InterceptingCall(nextCall(options)); + call.cancelWithStatus( + status.CANCELLED, + 'Unable to successfully retry request within client timeout' + ); + return call; + } else { + options.deadline = deadline; + this.logger.debug( + `new deadline set to ${options.deadline.valueOf()}` + ); + } + } + + return new InterceptingCall(nextCall(options)); + }; + } +} diff --git a/packages/client-sdk-nodejs/src/internal/grpc/storage-client-timeout-interceptor.ts b/packages/client-sdk-nodejs/src/internal/grpc/storage-client-timeout-interceptor.ts deleted file mode 100644 index 0716a6dcd..000000000 --- a/packages/client-sdk-nodejs/src/internal/grpc/storage-client-timeout-interceptor.ts +++ /dev/null @@ -1,89 +0,0 @@ -import {MomentoLogger, MomentoLoggerFactory} from '@gomomento/sdk-core'; -import {InterceptingCall, Interceptor, status} from '@grpc/grpc-js'; - -export function createStorageClientTimeoutInterceptor( - loggerFactory: MomentoLoggerFactory, - overallRequestTimeoutMs: number, - responseDataReceivedTimeoutMs: number -): Array { - return [ - new StorageClientTimeoutInterceptor( - loggerFactory, - overallRequestTimeoutMs, - responseDataReceivedTimeoutMs - ).createTimeoutInterceptor(), - ]; -} - -export class StorageClientTimeoutInterceptor { - private readonly logger: MomentoLogger; - private readonly responseDataReceivedTimeoutMs: number; - private readonly overallRequestTimeoutMs: number; - private overallDeadline?: Date; - - constructor( - loggerFactory: MomentoLoggerFactory, - overallRequestTimeoutMs: number, - responseDataReceivedTimeoutMs: number - ) { - this.logger = loggerFactory.getLogger(this); - this.responseDataReceivedTimeoutMs = responseDataReceivedTimeoutMs; - this.overallRequestTimeoutMs = overallRequestTimeoutMs; - } - - public createTimeoutInterceptor(): Interceptor { - return (options, nextCall) => { - // Replace default timeout with the desired overall timeout - // on the first time through and set the first incremental timeout - if (this.overallDeadline === undefined) { - const newDate = new Date(Date.now()); - newDate.setMilliseconds( - newDate.getMilliseconds() + this.overallRequestTimeoutMs - ); - this.overallDeadline = newDate; - - const deadline = new Date(Date.now()); - deadline.setMilliseconds( - deadline.getMilliseconds() + this.responseDataReceivedTimeoutMs - ); - options.deadline = deadline; - this.logger.debug(`new deadline set to ${options.deadline.valueOf()}`); - return new InterceptingCall(nextCall(options)); - } - - const receivedDeadline = options.deadline?.valueOf() || 0; - this.logger.debug( - `intercepting call with options.deadline ${receivedDeadline}` - ); - - // Reset incremental deadline only if it has been reached - if (receivedDeadline < Date.now()) { - this.logger.debug('received deadline < current time, resetting'); - - const deadline = new Date(Date.now()); - deadline.setMilliseconds( - deadline.getMilliseconds() + this.responseDataReceivedTimeoutMs - ); - - if (deadline.valueOf() > this.overallDeadline.valueOf()) { - this.logger.debug( - 'Unable to successfully retry request within client timeout, canceling request' - ); - const call = new InterceptingCall(nextCall(options)); - call.cancelWithStatus( - status.CANCELLED, - 'Unable to successfully retry request within client timeout' - ); - return call; - } else { - options.deadline = deadline; - this.logger.debug( - `new deadline set to ${options.deadline.valueOf()}` - ); - } - } - - return new InterceptingCall(nextCall(options)); - }; - } -} diff --git a/packages/client-sdk-nodejs/src/internal/storage-data-client.ts b/packages/client-sdk-nodejs/src/internal/storage-data-client.ts index 9c7c5ef33..0d95a502b 100644 --- a/packages/client-sdk-nodejs/src/internal/storage-data-client.ts +++ b/packages/client-sdk-nodejs/src/internal/storage-data-client.ts @@ -25,9 +25,7 @@ import {StorageConfiguration} from '../config/storage-configuration'; import {StorageClientPropsWithConfig} from './storage-client-props-with-config'; import {StaticGrpcConfiguration} from '../config/transport/cache'; import {CacheServiceErrorMapper} from '../errors/cache-service-error-mapper'; -import {createStorageClientTimeoutInterceptor} from './grpc/storage-client-timeout-interceptor'; import {createRetryInterceptorIfEnabled} from './grpc/retry-interceptor'; -import {DefaultStorageRetryStrategy} from '../config/retry/storage-default-retry-strategy'; import {ClientTimeoutInterceptor} from './grpc/client-timeout-interceptor'; export class StorageDataClient implements IStorageDataClient { @@ -115,22 +113,6 @@ export class StorageDataClient implements IStorageDataClient { new Header('runtime-version', `nodejs:${process.versions.node}`), ]; - // Determine which retry strategy is specified in the configuration - // and which interceptor to use. - const retryStrategy = this.configuration.getRetryStrategy(); - let timeoutInterceptor: Interceptor; - if (retryStrategy instanceof DefaultStorageRetryStrategy) { - const responseDataReceivedTimeoutMs = - retryStrategy.getResponseDataReceivedTimeoutMillis(); - timeoutInterceptor = createStorageClientTimeoutInterceptor( - _loggerFactory, - this.requestTimeoutMs, - responseDataReceivedTimeoutMs - )[0]; - } else { - timeoutInterceptor = ClientTimeoutInterceptor(this.requestTimeoutMs); - } - return [ ...createRetryInterceptorIfEnabled( this.configuration.getLoggerFactory(), @@ -138,7 +120,11 @@ export class StorageDataClient implements IStorageDataClient { ), new HeaderInterceptorProvider(headers).createHeadersInterceptor(), // For the timeout interceptors to work correctly, it must be specified last. - timeoutInterceptor, + ClientTimeoutInterceptor( + this.requestTimeoutMs, + this.configuration.getRetryStrategy(), + _loggerFactory + ), ]; } From 135595e3dafd7ec6841fd7e16f40a52a9ebe0cc6 Mon Sep 17 00:00:00 2001 From: anitarua Date: Wed, 21 Aug 2024 11:17:05 -0700 Subject: [PATCH 19/43] remove stale comment --- packages/client-sdk-nodejs/src/config/storage-configuration.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/client-sdk-nodejs/src/config/storage-configuration.ts b/packages/client-sdk-nodejs/src/config/storage-configuration.ts index 9614d913a..ecafd356c 100644 --- a/packages/client-sdk-nodejs/src/config/storage-configuration.ts +++ b/packages/client-sdk-nodejs/src/config/storage-configuration.ts @@ -1,9 +1,6 @@ import {MomentoLoggerFactory, RetryStrategy} from '../'; import {StorageTransportStrategy} from './transport/storage'; -// TODO: include ResponseDataReceivedTimeout as a top-level config? -// Or keep at the StorageTransportStrategy level? - /** * Configuration options for Momento StorageClient * From d4957712bbef28bb4617eb512b91c23614ee5a61 Mon Sep 17 00:00:00 2001 From: anitarua Date: Wed, 21 Aug 2024 12:37:14 -0700 Subject: [PATCH 20/43] update jitter formula --- .../client-sdk-nodejs/src/internal/grpc/retry-interceptor.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/client-sdk-nodejs/src/internal/grpc/retry-interceptor.ts b/packages/client-sdk-nodejs/src/internal/grpc/retry-interceptor.ts index aa5b2feab..82c39983c 100644 --- a/packages/client-sdk-nodejs/src/internal/grpc/retry-interceptor.ts +++ b/packages/client-sdk-nodejs/src/internal/grpc/retry-interceptor.ts @@ -26,7 +26,7 @@ export function createRetryInterceptorIfEnabled( } function addJitter(whenToRetry: number): number { - return (1 + Math.random()) * (1.2 * whenToRetry) - 1.1 * whenToRetry; + return (0.2 * Math.random() + 0.9) * whenToRetry; } export class RetryInterceptor { From 04234c1a9a80c777d5a40af513915c7cfb6112d3 Mon Sep 17 00:00:00 2001 From: anitarua Date: Wed, 21 Aug 2024 12:41:16 -0700 Subject: [PATCH 21/43] move jitter inside retry strategy --- .../retry/storage-default-retry-strategy.ts | 8 ++++++-- .../src/internal/grpc/retry-interceptor.ts | 19 ++++--------------- 2 files changed, 10 insertions(+), 17 deletions(-) diff --git a/packages/client-sdk-nodejs/src/config/retry/storage-default-retry-strategy.ts b/packages/client-sdk-nodejs/src/config/retry/storage-default-retry-strategy.ts index e1cffe0a0..08c798354 100644 --- a/packages/client-sdk-nodejs/src/config/retry/storage-default-retry-strategy.ts +++ b/packages/client-sdk-nodejs/src/config/retry/storage-default-retry-strategy.ts @@ -51,8 +51,8 @@ export class DefaultStorageRetryStrategy implements RetryStrategy { this.logger.debug( `Request is eligible for retry (attempt ${props.attemptNumber}), retrying soon.` ); - // retry after a fixed time interval has passed - return this.retryDelayIntervalMillis; + // retry after a fixed time interval has passed (=/- some jitter) + return addJitter(this.retryDelayIntervalMillis); } public getResponseDataReceivedTimeoutMillis(): number { @@ -73,3 +73,7 @@ export class DefaultStorageRetryStrategyFactory { }); } } + +export function addJitter(whenToRetry: number): number { + return (0.2 * Math.random() + 0.9) * whenToRetry; +} diff --git a/packages/client-sdk-nodejs/src/internal/grpc/retry-interceptor.ts b/packages/client-sdk-nodejs/src/internal/grpc/retry-interceptor.ts index 82c39983c..69deb5c7c 100644 --- a/packages/client-sdk-nodejs/src/internal/grpc/retry-interceptor.ts +++ b/packages/client-sdk-nodejs/src/internal/grpc/retry-interceptor.ts @@ -25,10 +25,6 @@ export function createRetryInterceptorIfEnabled( ]; } -function addJitter(whenToRetry: number): number { - return (0.2 * Math.random() + 0.9) * whenToRetry; -} - export class RetryInterceptor { private readonly logger: MomentoLogger; private readonly retryStrategy: RetryStrategy; @@ -44,8 +40,6 @@ export class RetryInterceptor { // TODO: We need to send retry count information to the server so that we // will have some visibility into how often this is happening to customers: // https://github.com/momentohq/client-sdk-nodejs/issues/80 - // TODO: we need to add backoff/jitter for the retries: - // https://github.com/momentohq/client-sdk-nodejs/issues/81 public createRetryInterceptor(): Interceptor { const logger = this.logger; const retryStrategy = this.retryStrategy; @@ -96,14 +90,10 @@ export class RetryInterceptor { savedMessageNext(savedReceiveMessage); next(status); } else { - const whenToRetryWithJitter = addJitter(whenToRetry); logger.debug( - `Request eligible for retry: path: ${options.method_definition.path}; response status code: ${status.code}; number of attempts (${attempts}); will retry in ${whenToRetryWithJitter}ms` - ); - setTimeout( - () => retry(message, metadata), - whenToRetryWithJitter + `Request eligible for retry: path: ${options.method_definition.path}; response status code: ${status.code}; number of attempts (${attempts}); will retry in ${whenToRetry}ms` ); + setTimeout(() => retry(message, metadata), whenToRetry); } }, }); @@ -127,13 +117,12 @@ export class RetryInterceptor { savedMessageNext(savedReceiveMessage); next(status); } else { - const whenToRetryWithJitter = addJitter(whenToRetry); logger.debug( - `Request eligible for retry: path: ${options.method_definition.path}; response status code: ${status.code}; number of attempts (${attempts}); will retry in ${whenToRetryWithJitter}ms` + `Request eligible for retry: path: ${options.method_definition.path}; response status code: ${status.code}; number of attempts (${attempts}); will retry in ${whenToRetry}ms` ); setTimeout( () => retry(savedSendMessage, savedMetadata), - whenToRetryWithJitter + whenToRetry ); } } From bc12f88041fa2388bd8993da5592374ab99cd86b Mon Sep 17 00:00:00 2001 From: anitarua Date: Wed, 21 Aug 2024 12:43:24 -0700 Subject: [PATCH 22/43] do not export internal interceptors --- .../src/internal/grpc/client-timeout-interceptor.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/client-sdk-nodejs/src/internal/grpc/client-timeout-interceptor.ts b/packages/client-sdk-nodejs/src/internal/grpc/client-timeout-interceptor.ts index 7183a1dac..64acfee50 100644 --- a/packages/client-sdk-nodejs/src/internal/grpc/client-timeout-interceptor.ts +++ b/packages/client-sdk-nodejs/src/internal/grpc/client-timeout-interceptor.ts @@ -28,7 +28,7 @@ export const ClientTimeoutInterceptor = ( ).createTimeoutInterceptor(); }; -export class BasicTimeoutInterceptor { +class BasicTimeoutInterceptor { private readonly overallRequestTimeoutMs: number; constructor(overallRequestTimeoutMs: number) { @@ -49,7 +49,7 @@ export class BasicTimeoutInterceptor { } } -export class RetryUntilTimeoutInterceptor { +class RetryUntilTimeoutInterceptor { private readonly logger: MomentoLogger; private readonly responseDataReceivedTimeoutMs: number; private readonly overallRequestTimeoutMs: number; From c4fc89fc44e18b388b7e2fcd6743098b48967658 Mon Sep 17 00:00:00 2001 From: anitarua Date: Wed, 21 Aug 2024 12:46:39 -0700 Subject: [PATCH 23/43] rename to FixedTimeoutRetryStrategy --- ...rategy.ts => fixed-timeout-retry-strategy.ts} | 16 ++++++++-------- .../src/config/storage-configurations.ts | 4 ++-- .../internal/grpc/client-timeout-interceptor.ts | 4 ++-- .../test/unit/storage-retry.test.ts | 10 ++++------ 4 files changed, 16 insertions(+), 18 deletions(-) rename packages/client-sdk-nodejs/src/config/retry/{storage-default-retry-strategy.ts => fixed-timeout-retry-strategy.ts} (87%) diff --git a/packages/client-sdk-nodejs/src/config/retry/storage-default-retry-strategy.ts b/packages/client-sdk-nodejs/src/config/retry/fixed-timeout-retry-strategy.ts similarity index 87% rename from packages/client-sdk-nodejs/src/config/retry/storage-default-retry-strategy.ts rename to packages/client-sdk-nodejs/src/config/retry/fixed-timeout-retry-strategy.ts index 08c798354..c5465f624 100644 --- a/packages/client-sdk-nodejs/src/config/retry/storage-default-retry-strategy.ts +++ b/packages/client-sdk-nodejs/src/config/retry/fixed-timeout-retry-strategy.ts @@ -7,10 +7,10 @@ import { MomentoLoggerFactory, MomentoLogger, DefaultMomentoLoggerFactory, -} from '../../'; +} from '../..'; import {DefaultStorageEligibilityStrategy} from './storage-default-eligibility-strategy'; -export interface DefaultStorageRetryStrategyProps { +export interface FixedTimeoutRetryStrategyProps { loggerFactory: MomentoLoggerFactory; eligibilityStrategy?: EligibilityStrategy; @@ -21,13 +21,13 @@ export interface DefaultStorageRetryStrategyProps { responseDataReceivedTimeoutMillis?: number; } -export class DefaultStorageRetryStrategy implements RetryStrategy { +export class FixedTimeoutRetryStrategy implements RetryStrategy { private readonly logger: MomentoLogger; private readonly eligibilityStrategy: EligibilityStrategy; private readonly retryDelayIntervalMillis: number; private readonly responseDataReceivedTimeoutMillis: number; - constructor(props: DefaultStorageRetryStrategyProps) { + constructor(props: FixedTimeoutRetryStrategyProps) { this.logger = props.loggerFactory.getLogger(this); this.eligibilityStrategy = props.eligibilityStrategy ?? @@ -60,11 +60,11 @@ export class DefaultStorageRetryStrategy implements RetryStrategy { } } -export class DefaultStorageRetryStrategyFactory { +export class FixedTimeoutRetryStrategyFactory { static getRetryStrategy( - props?: DefaultStorageRetryStrategyProps - ): DefaultStorageRetryStrategy { - return new DefaultStorageRetryStrategy({ + props?: FixedTimeoutRetryStrategyProps + ): FixedTimeoutRetryStrategy { + return new FixedTimeoutRetryStrategy({ loggerFactory: props?.loggerFactory ?? new DefaultMomentoLoggerFactory(), eligibilityStrategy: props?.eligibilityStrategy, retryDelayIntervalMillis: props?.retryDelayIntervalMillis, diff --git a/packages/client-sdk-nodejs/src/config/storage-configurations.ts b/packages/client-sdk-nodejs/src/config/storage-configurations.ts index 458ae5989..7ad9b09d9 100644 --- a/packages/client-sdk-nodejs/src/config/storage-configurations.ts +++ b/packages/client-sdk-nodejs/src/config/storage-configurations.ts @@ -8,7 +8,7 @@ import { StaticStorageGrpcConfiguration, StaticStorageTransportStrategy, } from './transport/storage'; -import {DefaultStorageRetryStrategy} from './retry/storage-default-retry-strategy'; +import {FixedTimeoutRetryStrategy} from './retry/fixed-timeout-retry-strategy'; import {RetryStrategy} from './retry/retry-strategy'; const defaultLoggerFactory: MomentoLoggerFactory = @@ -17,7 +17,7 @@ const defaultLoggerFactory: MomentoLoggerFactory = function defaultRetryStrategy( loggerFactory: MomentoLoggerFactory ): RetryStrategy { - return new DefaultStorageRetryStrategy({ + return new FixedTimeoutRetryStrategy({ loggerFactory: loggerFactory, }); } diff --git a/packages/client-sdk-nodejs/src/internal/grpc/client-timeout-interceptor.ts b/packages/client-sdk-nodejs/src/internal/grpc/client-timeout-interceptor.ts index 64acfee50..b58a0683d 100644 --- a/packages/client-sdk-nodejs/src/internal/grpc/client-timeout-interceptor.ts +++ b/packages/client-sdk-nodejs/src/internal/grpc/client-timeout-interceptor.ts @@ -1,7 +1,7 @@ import {MomentoLogger, MomentoLoggerFactory} from '@gomomento/sdk-core'; import {InterceptingCall, Interceptor, status} from '@grpc/grpc-js'; import {RetryStrategy} from '../../config/retry/retry-strategy'; -import {DefaultStorageRetryStrategy} from '../../config/retry/storage-default-retry-strategy'; +import {FixedTimeoutRetryStrategy} from '../../config/retry/fixed-timeout-retry-strategy'; import {DefaultMomentoLoggerFactory} from '../../config/logging/default-momento-logger'; // Determine which retry strategy is specified in the configuration @@ -13,7 +13,7 @@ export const ClientTimeoutInterceptor = ( ): Interceptor => { if ( retryStrategy !== undefined && - retryStrategy instanceof DefaultStorageRetryStrategy + retryStrategy instanceof FixedTimeoutRetryStrategy ) { const responseDataReceivedTimeoutMs = retryStrategy.getResponseDataReceivedTimeoutMillis(); diff --git a/packages/client-sdk-nodejs/test/unit/storage-retry.test.ts b/packages/client-sdk-nodejs/test/unit/storage-retry.test.ts index 23b014c2a..466f2aa18 100644 --- a/packages/client-sdk-nodejs/test/unit/storage-retry.test.ts +++ b/packages/client-sdk-nodejs/test/unit/storage-retry.test.ts @@ -11,7 +11,7 @@ import { StaticStorageTransportStrategy, StorageClientConfiguration, } from '../../src'; -import {DefaultStorageRetryStrategyFactory} from '../../src/config/retry/storage-default-retry-strategy'; +import {FixedTimeoutRetryStrategyFactory} from '../../src/config/retry/fixed-timeout-retry-strategy'; import {Metadata, StatusObject} from '@grpc/grpc-js'; describe('storage configuration', () => { @@ -22,11 +22,9 @@ describe('storage configuration', () => { const testTransportStrategy = new StaticStorageTransportStrategy({ grpcConfiguration: testGrpcConfiguration, }); - const testRetryStrategy = DefaultStorageRetryStrategyFactory.getRetryStrategy( - { - loggerFactory: testLoggerFactory, - } - ); + const testRetryStrategy = FixedTimeoutRetryStrategyFactory.getRetryStrategy({ + loggerFactory: testLoggerFactory, + }); const testConfiguration = new StorageClientConfiguration({ loggerFactory: testLoggerFactory, transportStrategy: testTransportStrategy, From d76e9454620e9a3c955e493f2c963b21ca74bb0c Mon Sep 17 00:00:00 2001 From: anitarua Date: Wed, 21 Aug 2024 13:05:53 -0700 Subject: [PATCH 24/43] fix unit test to account for jitter --- .../test/unit/storage-retry.test.ts | 72 +++++++++---------- 1 file changed, 36 insertions(+), 36 deletions(-) diff --git a/packages/client-sdk-nodejs/test/unit/storage-retry.test.ts b/packages/client-sdk-nodejs/test/unit/storage-retry.test.ts index 466f2aa18..dbcf11177 100644 --- a/packages/client-sdk-nodejs/test/unit/storage-retry.test.ts +++ b/packages/client-sdk-nodejs/test/unit/storage-retry.test.ts @@ -90,54 +90,54 @@ describe('storage configuration', () => { details: 'deadline exceeded', metadata: new Metadata(), }; - expect( - testRetryStrategy - .determineWhenToRetryRequest({ - grpcStatus: testGrpcStatusDeadlineExceeded, - grpcRequest: testClientRequest as ClientMethodDefinition< - unknown, - unknown - >, - attemptNumber: 1, - }) - ?.valueOf() - ).toEqual(100); // will retry after 100ms + const whenToRetryDeadlineExceeded = + testRetryStrategy.determineWhenToRetryRequest({ + grpcStatus: testGrpcStatusDeadlineExceeded, + grpcRequest: testClientRequest as ClientMethodDefinition< + unknown, + unknown + >, + attemptNumber: 1, + }); + // should retry after 100ms +/- 10% jitter + expect(whenToRetryDeadlineExceeded).toBeGreaterThanOrEqual(90); + expect(whenToRetryDeadlineExceeded).toBeLessThanOrEqual(110); const testGrpcStatusInternalError = { code: Status.INTERNAL, details: 'internal server error', metadata: new Metadata(), }; - expect( - testRetryStrategy - .determineWhenToRetryRequest({ - grpcStatus: testGrpcStatusInternalError, - grpcRequest: testClientRequest as ClientMethodDefinition< - unknown, - unknown - >, - attemptNumber: 1, - }) - ?.valueOf() - ).toEqual(100); // will retry after 100ms + const whenToRetryInternalError = + testRetryStrategy.determineWhenToRetryRequest({ + grpcStatus: testGrpcStatusInternalError, + grpcRequest: testClientRequest as ClientMethodDefinition< + unknown, + unknown + >, + attemptNumber: 1, + }); + // should retry after 100ms +/- 10% jitter + expect(whenToRetryInternalError).toBeGreaterThanOrEqual(90); + expect(whenToRetryInternalError).toBeLessThanOrEqual(110); const testGrpcStatusUnavailable = { code: Status.UNAVAILABLE, details: 'server unavailable', metadata: new Metadata(), }; - expect( - testRetryStrategy - .determineWhenToRetryRequest({ - grpcStatus: testGrpcStatusUnavailable, - grpcRequest: testClientRequest as ClientMethodDefinition< - unknown, - unknown - >, - attemptNumber: 1, - }) - ?.valueOf() - ).toEqual(100); // will retry after 100ms + const whenToRetryUnavailable = + testRetryStrategy.determineWhenToRetryRequest({ + grpcStatus: testGrpcStatusUnavailable, + grpcRequest: testClientRequest as ClientMethodDefinition< + unknown, + unknown + >, + attemptNumber: 1, + }); + // should retry after 100ms +/- 10% jitter + expect(whenToRetryUnavailable).toBeGreaterThanOrEqual(90); + expect(whenToRetryUnavailable).toBeLessThanOrEqual(110); const testGrpcStatusCancelled = { code: Status.CANCELLED, From 352cf8f72e6c61f74a14e60a8285b2a600c87b80 Mon Sep 17 00:00:00 2001 From: anitarua Date: Wed, 21 Aug 2024 15:35:11 -0700 Subject: [PATCH 25/43] simplify timeout interceptor --- .../grpc/client-timeout-interceptor.ts | 59 ++++++++++--------- 1 file changed, 31 insertions(+), 28 deletions(-) diff --git a/packages/client-sdk-nodejs/src/internal/grpc/client-timeout-interceptor.ts b/packages/client-sdk-nodejs/src/internal/grpc/client-timeout-interceptor.ts index b58a0683d..54bbe0a70 100644 --- a/packages/client-sdk-nodejs/src/internal/grpc/client-timeout-interceptor.ts +++ b/packages/client-sdk-nodejs/src/internal/grpc/client-timeout-interceptor.ts @@ -85,36 +85,39 @@ class RetryUntilTimeoutInterceptor { return new InterceptingCall(nextCall(options)); } - const receivedDeadline = options.deadline?.valueOf() || 0; - this.logger.debug( - `intercepting call with options.deadline ${receivedDeadline}` - ); - - // Reset incremental deadline only if it has been reached - if (receivedDeadline < Date.now()) { - this.logger.debug('received deadline < current time, resetting'); - - const deadline = new Date(Date.now()); - deadline.setMilliseconds( - deadline.getMilliseconds() + this.responseDataReceivedTimeoutMs + // If the received deadline is equal to the overall deadline, we've + // maxed out the retries and should cancel the request. + const receivedDeadline = options.deadline; + if ( + receivedDeadline === undefined || + receivedDeadline === this.overallDeadline + ) { + this.logger.debug( + 'Unable to successfully retry request within overall timeout, canceling request' + ); + // reset overall deadline for next request + this.overallDeadline = undefined; + const call = new InterceptingCall(nextCall(options)); + call.cancelWithStatus( + status.CANCELLED, + 'Unable to successfully retry request within overall timeout' ); + return call; + } - if (deadline.valueOf() > this.overallDeadline.valueOf()) { - this.logger.debug( - 'Unable to successfully retry request within client timeout, canceling request' - ); - const call = new InterceptingCall(nextCall(options)); - call.cancelWithStatus( - status.CANCELLED, - 'Unable to successfully retry request within client timeout' - ); - return call; - } else { - options.deadline = deadline; - this.logger.debug( - `new deadline set to ${options.deadline.valueOf()}` - ); - } + // Otherwise, we've hit an incremental timeout and must set the next deadline. + const newDeadline = new Date(Date.now()); + newDeadline.setMilliseconds( + newDeadline.getMilliseconds() + this.responseDataReceivedTimeoutMs + ); + if (newDeadline > this.overallDeadline) { + this.logger.debug( + `new deadline would exceed overall deadline, setting to overall deadline ${this.overallDeadline.valueOf()}` + ); + options.deadline = this.overallDeadline; + } else { + this.logger.debug(`new deadline set to ${newDeadline.valueOf()}`); + options.deadline = newDeadline; } return new InterceptingCall(nextCall(options)); From a0e7bd6ef08d6d2a05bdc9a0b4278a97fc1ac6c9 Mon Sep 17 00:00:00 2001 From: anitarua Date: Wed, 21 Aug 2024 15:42:04 -0700 Subject: [PATCH 26/43] add optional property to retry strategy --- .../src/config/retry/fixed-timeout-retry-strategy.ts | 6 +----- .../client-sdk-nodejs/src/config/retry/retry-strategy.ts | 2 ++ .../src/internal/grpc/client-timeout-interceptor.ts | 7 ++----- 3 files changed, 5 insertions(+), 10 deletions(-) diff --git a/packages/client-sdk-nodejs/src/config/retry/fixed-timeout-retry-strategy.ts b/packages/client-sdk-nodejs/src/config/retry/fixed-timeout-retry-strategy.ts index c5465f624..a8929c7b1 100644 --- a/packages/client-sdk-nodejs/src/config/retry/fixed-timeout-retry-strategy.ts +++ b/packages/client-sdk-nodejs/src/config/retry/fixed-timeout-retry-strategy.ts @@ -25,7 +25,7 @@ export class FixedTimeoutRetryStrategy implements RetryStrategy { private readonly logger: MomentoLogger; private readonly eligibilityStrategy: EligibilityStrategy; private readonly retryDelayIntervalMillis: number; - private readonly responseDataReceivedTimeoutMillis: number; + readonly responseDataReceivedTimeoutMillis: number; constructor(props: FixedTimeoutRetryStrategyProps) { this.logger = props.loggerFactory.getLogger(this); @@ -54,10 +54,6 @@ export class FixedTimeoutRetryStrategy implements RetryStrategy { // retry after a fixed time interval has passed (=/- some jitter) return addJitter(this.retryDelayIntervalMillis); } - - public getResponseDataReceivedTimeoutMillis(): number { - return this.responseDataReceivedTimeoutMillis; - } } export class FixedTimeoutRetryStrategyFactory { diff --git a/packages/client-sdk-nodejs/src/config/retry/retry-strategy.ts b/packages/client-sdk-nodejs/src/config/retry/retry-strategy.ts index e5c35518b..1b641d528 100644 --- a/packages/client-sdk-nodejs/src/config/retry/retry-strategy.ts +++ b/packages/client-sdk-nodejs/src/config/retry/retry-strategy.ts @@ -9,6 +9,8 @@ export interface DeterminewhenToRetryRequestProps { } export interface RetryStrategy { + responseDataReceivedTimeoutMillis?: number; + determineWhenToRetryRequest( props: DeterminewhenToRetryRequestProps ): number | null; diff --git a/packages/client-sdk-nodejs/src/internal/grpc/client-timeout-interceptor.ts b/packages/client-sdk-nodejs/src/internal/grpc/client-timeout-interceptor.ts index 54bbe0a70..0c476b6dc 100644 --- a/packages/client-sdk-nodejs/src/internal/grpc/client-timeout-interceptor.ts +++ b/packages/client-sdk-nodejs/src/internal/grpc/client-timeout-interceptor.ts @@ -1,7 +1,6 @@ import {MomentoLogger, MomentoLoggerFactory} from '@gomomento/sdk-core'; import {InterceptingCall, Interceptor, status} from '@grpc/grpc-js'; import {RetryStrategy} from '../../config/retry/retry-strategy'; -import {FixedTimeoutRetryStrategy} from '../../config/retry/fixed-timeout-retry-strategy'; import {DefaultMomentoLoggerFactory} from '../../config/logging/default-momento-logger'; // Determine which retry strategy is specified in the configuration @@ -13,14 +12,12 @@ export const ClientTimeoutInterceptor = ( ): Interceptor => { if ( retryStrategy !== undefined && - retryStrategy instanceof FixedTimeoutRetryStrategy + retryStrategy.responseDataReceivedTimeoutMillis !== undefined ) { - const responseDataReceivedTimeoutMs = - retryStrategy.getResponseDataReceivedTimeoutMillis(); return new RetryUntilTimeoutInterceptor( loggerFactory ?? new DefaultMomentoLoggerFactory(), overallRequestTimeoutMs, - responseDataReceivedTimeoutMs + retryStrategy.responseDataReceivedTimeoutMillis ).createTimeoutInterceptor(); } return new BasicTimeoutInterceptor( From cec9758c3cecb1e64737b95ac3c9d85d7f9fd5d4 Mon Sep 17 00:00:00 2001 From: anitarua Date: Wed, 21 Aug 2024 16:28:08 -0700 Subject: [PATCH 27/43] use expectWithMessage in failing test, interceptor should not cancel if received deadline is undefined --- .../grpc/client-timeout-interceptor.ts | 2 +- .../src/storage/storage.ts | 26 ++++++++++++++----- 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/packages/client-sdk-nodejs/src/internal/grpc/client-timeout-interceptor.ts b/packages/client-sdk-nodejs/src/internal/grpc/client-timeout-interceptor.ts index 0c476b6dc..44620951d 100644 --- a/packages/client-sdk-nodejs/src/internal/grpc/client-timeout-interceptor.ts +++ b/packages/client-sdk-nodejs/src/internal/grpc/client-timeout-interceptor.ts @@ -86,7 +86,7 @@ class RetryUntilTimeoutInterceptor { // maxed out the retries and should cancel the request. const receivedDeadline = options.deadline; if ( - receivedDeadline === undefined || + receivedDeadline !== undefined && receivedDeadline === this.overallDeadline ) { this.logger.debug( diff --git a/packages/common-integration-tests/src/storage/storage.ts b/packages/common-integration-tests/src/storage/storage.ts index ca550e470..21794c1fd 100644 --- a/packages/common-integration-tests/src/storage/storage.ts +++ b/packages/common-integration-tests/src/storage/storage.ts @@ -8,7 +8,11 @@ import { StorageGetResponse, StoragePutResponse, } from '@gomomento/sdk-core'; -import {testStoreName, WithStore} from '../common-int-test-utils'; +import { + expectWithMessage, + testStoreName, + WithStore, +} from '../common-int-test-utils'; import {v4} from 'uuid'; export function runStorageServiceTests( @@ -123,7 +127,9 @@ export function runStorageServiceTests( } } const getIntResponse = await storageClient.get(testingStoreName, key); - expect(getIntResponse.type).toEqual(StorageGetResponse.Found); + expectWithMessage(() => { + expect(getIntResponse.type).toEqual(StorageGetResponse.Found); + }, `expected Found, received ${getIntResponse.toString()}`); expect(getIntResponse.value()?.int()).toEqual(intValue); // put/get a double value @@ -147,7 +153,9 @@ export function runStorageServiceTests( testingStoreName, key ); - expect(getDoubleResponse.type).toEqual(StorageGetResponse.Found); + expectWithMessage(() => { + expect(getDoubleResponse.type).toEqual(StorageGetResponse.Found); + }, `expected Found, received ${getDoubleResponse.toString()}`); expect(getDoubleResponse.value()?.double()).toEqual(doubleValue); // put/get a string value @@ -171,7 +179,9 @@ export function runStorageServiceTests( testingStoreName, key ); - expect(getStringResponse.type).toEqual(StorageGetResponse.Found); + expectWithMessage(() => { + expect(getStringResponse.type).toEqual(StorageGetResponse.Found); + }, `expected Found, received ${getStringResponse.toString()}`); expect(getStringResponse.value()?.string()).toEqual(stringValue); // put/get a bytes value @@ -192,7 +202,9 @@ export function runStorageServiceTests( } } const getBytesResponse = await storageClient.get(testingStoreName, key); - expect(getBytesResponse.type).toEqual(StorageGetResponse.Found); + expectWithMessage(() => { + expect(getBytesResponse.type).toEqual(StorageGetResponse.Found); + }, `expected Found, received ${getBytesResponse.toString()}`); expect(getBytesResponse.value()?.bytes()).toEqual(bytesValue); const deleteResponse = await storageClient.delete( @@ -215,7 +227,9 @@ export function runStorageServiceTests( await WithStore(storageClient, testingStoreName, async () => { const key = v4(); const getResponse = await storageClient.get(testingStoreName, key); - expect(getResponse.type).toEqual(StorageGetResponse.NotFound); + expectWithMessage(() => { + expect(getResponse.type).toEqual(StorageGetResponse.NotFound); + }, `expected NotFound, received ${getResponse.toString()}`); expect(getResponse.value()).toBeUndefined(); }); }); From 1ab9453160177b4a03cfc77857bf45316d5d38c7 Mon Sep 17 00:00:00 2001 From: Anita Ruangrotsakun <138700973+anitarua@users.noreply.github.com> Date: Thu, 22 Aug 2024 09:59:34 -0700 Subject: [PATCH 28/43] fix typo in jitter comment Co-authored-by: Michael Landis --- .../src/config/retry/fixed-timeout-retry-strategy.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/client-sdk-nodejs/src/config/retry/fixed-timeout-retry-strategy.ts b/packages/client-sdk-nodejs/src/config/retry/fixed-timeout-retry-strategy.ts index a8929c7b1..8b348262e 100644 --- a/packages/client-sdk-nodejs/src/config/retry/fixed-timeout-retry-strategy.ts +++ b/packages/client-sdk-nodejs/src/config/retry/fixed-timeout-retry-strategy.ts @@ -51,7 +51,7 @@ export class FixedTimeoutRetryStrategy implements RetryStrategy { this.logger.debug( `Request is eligible for retry (attempt ${props.attemptNumber}), retrying soon.` ); - // retry after a fixed time interval has passed (=/- some jitter) + // retry after a fixed time interval has passed (+/- some jitter) return addJitter(this.retryDelayIntervalMillis); } } From e6cc11b93fa062c4b7d176a2a9554dcc0b78751c Mon Sep 17 00:00:00 2001 From: anitarua Date: Thu, 22 Aug 2024 10:11:11 -0700 Subject: [PATCH 29/43] DRY up interceptor code, create helper function for creating new deadline --- .../grpc/client-timeout-interceptor.ts | 24 ++++++++----------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/packages/client-sdk-nodejs/src/internal/grpc/client-timeout-interceptor.ts b/packages/client-sdk-nodejs/src/internal/grpc/client-timeout-interceptor.ts index 44620951d..8e2f9b57c 100644 --- a/packages/client-sdk-nodejs/src/internal/grpc/client-timeout-interceptor.ts +++ b/packages/client-sdk-nodejs/src/internal/grpc/client-timeout-interceptor.ts @@ -67,17 +67,10 @@ class RetryUntilTimeoutInterceptor { // Replace default timeout with the desired overall timeout // on the first time through and set the first incremental timeout if (this.overallDeadline === undefined) { - const newDate = new Date(Date.now()); - newDate.setMilliseconds( - newDate.getMilliseconds() + this.overallRequestTimeoutMs + this.overallDeadline = createNewDeadline(this.overallRequestTimeoutMs); + options.deadline = createNewDeadline( + this.responseDataReceivedTimeoutMs ); - this.overallDeadline = newDate; - - const deadline = new Date(Date.now()); - deadline.setMilliseconds( - deadline.getMilliseconds() + this.responseDataReceivedTimeoutMs - ); - options.deadline = deadline; this.logger.debug(`new deadline set to ${options.deadline.valueOf()}`); return new InterceptingCall(nextCall(options)); } @@ -103,10 +96,7 @@ class RetryUntilTimeoutInterceptor { } // Otherwise, we've hit an incremental timeout and must set the next deadline. - const newDeadline = new Date(Date.now()); - newDeadline.setMilliseconds( - newDeadline.getMilliseconds() + this.responseDataReceivedTimeoutMs - ); + const newDeadline = createNewDeadline(this.responseDataReceivedTimeoutMs); if (newDeadline > this.overallDeadline) { this.logger.debug( `new deadline would exceed overall deadline, setting to overall deadline ${this.overallDeadline.valueOf()}` @@ -121,3 +111,9 @@ class RetryUntilTimeoutInterceptor { }; } } + +function createNewDeadline(timeToAddMillis: number): Date { + const deadline = new Date(Date.now()); + deadline.setMilliseconds(deadline.getMilliseconds() + timeToAddMillis); + return deadline; +} From 3cfd8c3b350215bd096128c2d4760d53bfb612aa Mon Sep 17 00:00:00 2001 From: anitarua Date: Thu, 22 Aug 2024 12:02:45 -0700 Subject: [PATCH 30/43] revert renamings --- .../src/config/storage-configuration.ts | 8 ++++---- .../config/transport/storage/transport-strategy.ts | 12 ++++++------ 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/client-sdk-nodejs/src/config/storage-configuration.ts b/packages/client-sdk-nodejs/src/config/storage-configuration.ts index ecafd356c..8af6ad960 100644 --- a/packages/client-sdk-nodejs/src/config/storage-configuration.ts +++ b/packages/client-sdk-nodejs/src/config/storage-configuration.ts @@ -25,10 +25,10 @@ export interface StorageConfiguration { /** * Convenience copy constructor that updates the client-side timeout setting in the TransportStrategy - * @param {number} requestTimeoutMillis + * @param {number} clientTimeoutMillis * @returns {StorageConfiguration} a new Configuration object with its TransportStrategy updated to use the specified client timeout */ - withRequestTimeoutMillis(requestTimeoutMillis: number): StorageConfiguration; + withClientTimeoutMillis(clientTimeoutMillis: number): StorageConfiguration; /** * Copy constructor for overriding TransportStrategy @@ -85,11 +85,11 @@ export class StorageClientConfiguration implements StorageConfiguration { return this.retryStrategy; } - withRequestTimeoutMillis(requestTimeoutMillis: number): StorageConfiguration { + withClientTimeoutMillis(clientTimeoutMillis: number): StorageConfiguration { return new StorageClientConfiguration({ loggerFactory: this.loggerFactory, transportStrategy: - this.transportStrategy.withRequestTimeoutMillis(requestTimeoutMillis), + this.transportStrategy.withClientTimeoutMillis(clientTimeoutMillis), retryStrategy: this.retryStrategy, }); } diff --git a/packages/client-sdk-nodejs/src/config/transport/storage/transport-strategy.ts b/packages/client-sdk-nodejs/src/config/transport/storage/transport-strategy.ts index 126071776..e3c45f01a 100644 --- a/packages/client-sdk-nodejs/src/config/transport/storage/transport-strategy.ts +++ b/packages/client-sdk-nodejs/src/config/transport/storage/transport-strategy.ts @@ -22,11 +22,11 @@ export interface StorageTransportStrategy { /** * Copy constructor to update the client-side timeout - * @param {number} requestTimeoutMillis + * @param {number} clientTimeoutMillis * @returns {StorageTransportStrategy} a new StorageTransportStrategy with the specified client timeout */ - withRequestTimeoutMillis( - requestTimeoutMillis: number + withClientTimeoutMillis( + clientTimeoutMillis: number ): StorageTransportStrategy; } @@ -78,12 +78,12 @@ export class StaticStorageTransportStrategy }); } - withRequestTimeoutMillis( - requestTimeoutMillis: number + withClientTimeoutMillis( + clientTimeoutMillis: number ): StorageTransportStrategy { return new StaticStorageTransportStrategy({ grpcConfiguration: - this.grpcConfig.withDeadlineMillis(requestTimeoutMillis), + this.grpcConfig.withDeadlineMillis(clientTimeoutMillis), }); } } From b68939ac918944fad3f6f19f0766455b73930c53 Mon Sep 17 00:00:00 2001 From: Chris Price Date: Fri, 23 Aug 2024 13:34:02 -0700 Subject: [PATCH 31/43] chore: add basic storage example --- examples/nodejs/storage/basic.ts | 65 ++++++++++++++++++++++++++++ examples/nodejs/storage/package.json | 1 + 2 files changed, 66 insertions(+) create mode 100644 examples/nodejs/storage/basic.ts diff --git a/examples/nodejs/storage/basic.ts b/examples/nodejs/storage/basic.ts new file mode 100644 index 000000000..cf81d325f --- /dev/null +++ b/examples/nodejs/storage/basic.ts @@ -0,0 +1,65 @@ +import { + CreateStoreResponse, + CredentialProvider, + PreviewStorageClient, + StorageConfigurations, + StorageGetResponse, + StoragePutResponse, +} from '@gomomento/sdk'; + +async function main() { + const storageClient = new PreviewStorageClient({ + configuration: StorageConfigurations.Laptop.latest(), + credentialProvider: CredentialProvider.fromEnvironmentVariable('MOMENTO_API_KEY'), + }); + + const storeName = 'my-store'; + const createStoreResponse = await storageClient.createStore(storeName); + switch (createStoreResponse.type) { + case CreateStoreResponse.AlreadyExists: + console.log(`Store '${storeName}' already exists`); + break; + case CreateStoreResponse.Success: + console.log(`Store '${storeName}' created`); + break; + case CreateStoreResponse.Error: + throw new Error( + `An error occurred while attempting to create store '${storeName}': ${createStoreResponse.errorCode()}: ${createStoreResponse.toString()}` + ); + } + + const putResponse = await storageClient.putString(storeName, 'test-key', 'test-value'); + switch (putResponse.type) { + case StoragePutResponse.Success: + console.log("Key 'test-key' stored successfully"); + break; + case StoragePutResponse.Error: + throw new Error( + `An error occurred while attempting to store key 'test-key' in store '${storeName}': ${putResponse.errorCode()}: ${putResponse.toString()}` + ); + } + + const getResponse = await storageClient.get(storeName, 'test-key'); + // simplified style; assume the value was found, and that it was a string + console.log(`string hit: ${getResponse.value()!.string()!}`); + + // pattern-matching style; safer for production code + switch (getResponse.type) { + case StorageGetResponse.Found: + // if you know the value is a string: + console.log(`Retrieved value for key 'test-key': ${getResponse.value().string()!}`); + break; + case StorageGetResponse.NotFound: + console.log(`Key 'test-key' was not found in store '${storeName}'`); + break; + case StorageGetResponse.Error: + throw new Error( + `An error occurred while attempting to get key 'test-key' from store '${storeName}': ${getResponse.errorCode()}: ${getResponse.toString()}` + ); + } +} + +main().catch(e => { + console.error('An error occurred! ', e); + throw e; +}); diff --git a/examples/nodejs/storage/package.json b/examples/nodejs/storage/package.json index ac721fa60..4ce56a36c 100644 --- a/examples/nodejs/storage/package.json +++ b/examples/nodejs/storage/package.json @@ -6,6 +6,7 @@ "scripts": { "prebuild": "eslint . --ext .ts", "build": "tsc", + "basic": "tsc && node dist/basic.js", "doc-examples": "tsc && node dist/doc-examples-js-apis.js", "validate-examples": "tsc && node dist/doc-examples-js-apis.js && node dist/cheat-sheet-main.js", "test": "jest", From 64b5f1371977874f372de66f160ae7ab35631ac0 Mon Sep 17 00:00:00 2001 From: anitarua Date: Fri, 23 Aug 2024 14:14:35 -0700 Subject: [PATCH 32/43] fix: missed an edge case in interceptor for resetting overall deadline --- .../grpc/client-timeout-interceptor.ts | 49 ++++++++++++++----- .../src/storage/storage.ts | 17 +++++++ 2 files changed, 53 insertions(+), 13 deletions(-) diff --git a/packages/client-sdk-nodejs/src/internal/grpc/client-timeout-interceptor.ts b/packages/client-sdk-nodejs/src/internal/grpc/client-timeout-interceptor.ts index 8e2f9b57c..fcc21aa9c 100644 --- a/packages/client-sdk-nodejs/src/internal/grpc/client-timeout-interceptor.ts +++ b/packages/client-sdk-nodejs/src/internal/grpc/client-timeout-interceptor.ts @@ -51,6 +51,7 @@ class RetryUntilTimeoutInterceptor { private readonly responseDataReceivedTimeoutMs: number; private readonly overallRequestTimeoutMs: number; private overallDeadline?: Date; + private previousRpc?: string; constructor( loggerFactory: MomentoLoggerFactory, @@ -64,19 +65,14 @@ class RetryUntilTimeoutInterceptor { public createTimeoutInterceptor(): Interceptor { return (options, nextCall) => { - // Replace default timeout with the desired overall timeout - // on the first time through and set the first incremental timeout - if (this.overallDeadline === undefined) { - this.overallDeadline = createNewDeadline(this.overallRequestTimeoutMs); - options.deadline = createNewDeadline( - this.responseDataReceivedTimeoutMs - ); - this.logger.debug(`new deadline set to ${options.deadline.valueOf()}`); - return new InterceptingCall(nextCall(options)); - } + this.logger.debug( + `Previous RPC: ${this.previousRpc ?? 'none'} | Incoming RPC: ${ + options.method_definition.path + }` + ); // If the received deadline is equal to the overall deadline, we've - // maxed out the retries and should cancel the request. + // maxed out the retries on a particular request and should cancel retries. const receivedDeadline = options.deadline; if ( receivedDeadline !== undefined && @@ -87,6 +83,7 @@ class RetryUntilTimeoutInterceptor { ); // reset overall deadline for next request this.overallDeadline = undefined; + this.previousRpc = undefined; const call = new InterceptingCall(nextCall(options)); call.cancelWithStatus( status.CANCELLED, @@ -95,17 +92,43 @@ class RetryUntilTimeoutInterceptor { return call; } + // Reset overall and incremental deadlines in these cases: + // Case 1: overallDeadline is undefined (first request or previous was canceled) + // Case 2: different RPC path requested through same client + // Case 3: new request through same client after original retry deadline has passed + // (Note: Case 3 check must occur after the receivedDeadline == options.deadline check + // or else the request is always retried). + if ( + this.overallDeadline === undefined || + this.previousRpc !== options.method_definition.path || + (this.overallDeadline !== undefined && + Date.now().valueOf() > this.overallDeadline.valueOf()) + ) { + this.previousRpc = options.method_definition.path; + this.overallDeadline = createNewDeadline(this.overallRequestTimeoutMs); + options.deadline = createNewDeadline( + this.responseDataReceivedTimeoutMs + ); + this.logger.debug( + `Overall deadline set to ${this.overallDeadline.valueOf()}; incremental deadline set to ${options.deadline.valueOf()}` + ); + return new InterceptingCall(nextCall(options)); + } + // Otherwise, we've hit an incremental timeout and must set the next deadline. const newDeadline = createNewDeadline(this.responseDataReceivedTimeoutMs); if (newDeadline > this.overallDeadline) { this.logger.debug( - `new deadline would exceed overall deadline, setting to overall deadline ${this.overallDeadline.valueOf()}` + `New incremental deadline ${newDeadline.valueOf()} would exceed overall deadline, setting to overall deadline ${this.overallDeadline.valueOf()}` ); options.deadline = this.overallDeadline; } else { - this.logger.debug(`new deadline set to ${newDeadline.valueOf()}`); + this.logger.debug( + `Incremental deadline set to ${newDeadline.valueOf()}` + ); options.deadline = newDeadline; } + this.previousRpc = options.method_definition.path; return new InterceptingCall(nextCall(options)); }; diff --git a/packages/common-integration-tests/src/storage/storage.ts b/packages/common-integration-tests/src/storage/storage.ts index 21794c1fd..34d14b8e2 100644 --- a/packages/common-integration-tests/src/storage/storage.ts +++ b/packages/common-integration-tests/src/storage/storage.ts @@ -14,6 +14,7 @@ import { WithStore, } from '../common-int-test-utils'; import {v4} from 'uuid'; +import {sleep} from '@gomomento/sdk-core/dist/src/internal/utils'; export function runStorageServiceTests( storageClient: IStorageClient, @@ -252,5 +253,21 @@ export function runStorageServiceTests( } } }); + it('should successfully make two of the same requests after 5s retry timeout', async () => { + await WithStore(storageClient, testingStoreName, async () => { + const key = v4(); + const getResponse1 = await storageClient.get(testingStoreName, key); + expectWithMessage(() => { + expect(getResponse1.type).toEqual(StorageGetResponse.NotFound); + }, `expected NotFound, received ${getResponse1.toString()}`); + + await sleep(5000); + + const getResponse2 = await storageClient.get(testingStoreName, key); + expectWithMessage(() => { + expect(getResponse2.type).toEqual(StorageGetResponse.NotFound); + }, `expected NotFound, received ${getResponse2.toString()}`); + }); + }); }); } From dbc84d583411af0b23c13fbff17b2ad1048a2f52 Mon Sep 17 00:00:00 2001 From: anitarua Date: Fri, 23 Aug 2024 14:18:30 -0700 Subject: [PATCH 33/43] no need to export addJitter --- .../src/config/retry/fixed-timeout-retry-strategy.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/client-sdk-nodejs/src/config/retry/fixed-timeout-retry-strategy.ts b/packages/client-sdk-nodejs/src/config/retry/fixed-timeout-retry-strategy.ts index 8b348262e..07735acea 100644 --- a/packages/client-sdk-nodejs/src/config/retry/fixed-timeout-retry-strategy.ts +++ b/packages/client-sdk-nodejs/src/config/retry/fixed-timeout-retry-strategy.ts @@ -70,6 +70,6 @@ export class FixedTimeoutRetryStrategyFactory { } } -export function addJitter(whenToRetry: number): number { +function addJitter(whenToRetry: number): number { return (0.2 * Math.random() + 0.9) * whenToRetry; } From f75c11fef6d1d280fdfe6ffaa1ddb0237d2f0e59 Mon Sep 17 00:00:00 2001 From: anitarua Date: Fri, 23 Aug 2024 14:21:25 -0700 Subject: [PATCH 34/43] make sure to export FixedTimeoutRetryStrategy --- packages/client-sdk-nodejs/src/index.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/client-sdk-nodejs/src/index.ts b/packages/client-sdk-nodejs/src/index.ts index 035696032..1d758ad35 100644 --- a/packages/client-sdk-nodejs/src/index.ts +++ b/packages/client-sdk-nodejs/src/index.ts @@ -214,6 +214,11 @@ export { FixedCountRetryStrategyProps, } from './config/retry/fixed-count-retry-strategy'; +export { + FixedTimeoutRetryStrategy, + FixedTimeoutRetryStrategyProps, +} from './config/retry/fixed-timeout-retry-strategy'; + export {DefaultEligibilityStrategy} from './config/retry/default-eligibility-strategy'; export { From 8a2d1c8c398e0ec6159827268fc08ec3a146f8c8 Mon Sep 17 00:00:00 2001 From: anitarua Date: Fri, 23 Aug 2024 14:25:22 -0700 Subject: [PATCH 35/43] we can ditch the unnecessary factory method --- .../retry/fixed-timeout-retry-strategy.ts | 20 +------------------ .../test/unit/storage-retry.test.ts | 4 ++-- 2 files changed, 3 insertions(+), 21 deletions(-) diff --git a/packages/client-sdk-nodejs/src/config/retry/fixed-timeout-retry-strategy.ts b/packages/client-sdk-nodejs/src/config/retry/fixed-timeout-retry-strategy.ts index 07735acea..9beaaebb4 100644 --- a/packages/client-sdk-nodejs/src/config/retry/fixed-timeout-retry-strategy.ts +++ b/packages/client-sdk-nodejs/src/config/retry/fixed-timeout-retry-strategy.ts @@ -3,11 +3,7 @@ import { RetryStrategy, } from './retry-strategy'; import {EligibilityStrategy} from './eligibility-strategy'; -import { - MomentoLoggerFactory, - MomentoLogger, - DefaultMomentoLoggerFactory, -} from '../..'; +import {MomentoLoggerFactory, MomentoLogger} from '../..'; import {DefaultStorageEligibilityStrategy} from './storage-default-eligibility-strategy'; export interface FixedTimeoutRetryStrategyProps { @@ -56,20 +52,6 @@ export class FixedTimeoutRetryStrategy implements RetryStrategy { } } -export class FixedTimeoutRetryStrategyFactory { - static getRetryStrategy( - props?: FixedTimeoutRetryStrategyProps - ): FixedTimeoutRetryStrategy { - return new FixedTimeoutRetryStrategy({ - loggerFactory: props?.loggerFactory ?? new DefaultMomentoLoggerFactory(), - eligibilityStrategy: props?.eligibilityStrategy, - retryDelayIntervalMillis: props?.retryDelayIntervalMillis, - responseDataReceivedTimeoutMillis: - props?.responseDataReceivedTimeoutMillis, - }); - } -} - function addJitter(whenToRetry: number): number { return (0.2 * Math.random() + 0.9) * whenToRetry; } diff --git a/packages/client-sdk-nodejs/test/unit/storage-retry.test.ts b/packages/client-sdk-nodejs/test/unit/storage-retry.test.ts index dbcf11177..5d35018df 100644 --- a/packages/client-sdk-nodejs/test/unit/storage-retry.test.ts +++ b/packages/client-sdk-nodejs/test/unit/storage-retry.test.ts @@ -7,11 +7,11 @@ import { import { DefaultMomentoLoggerFactory, FixedCountRetryStrategy, + FixedTimeoutRetryStrategy, StaticStorageGrpcConfiguration, StaticStorageTransportStrategy, StorageClientConfiguration, } from '../../src'; -import {FixedTimeoutRetryStrategyFactory} from '../../src/config/retry/fixed-timeout-retry-strategy'; import {Metadata, StatusObject} from '@grpc/grpc-js'; describe('storage configuration', () => { @@ -22,7 +22,7 @@ describe('storage configuration', () => { const testTransportStrategy = new StaticStorageTransportStrategy({ grpcConfiguration: testGrpcConfiguration, }); - const testRetryStrategy = FixedTimeoutRetryStrategyFactory.getRetryStrategy({ + const testRetryStrategy = new FixedTimeoutRetryStrategy({ loggerFactory: testLoggerFactory, }); const testConfiguration = new StorageClientConfiguration({ From 8451b1008c68d30754ccf3342d0898463580edba Mon Sep 17 00:00:00 2001 From: anitarua Date: Fri, 23 Aug 2024 14:34:21 -0700 Subject: [PATCH 36/43] add more info to logs, fix attempt number in retry interceptor lo gs --- .../retry/fixed-timeout-retry-strategy.ts | 2 +- .../internal/grpc/client-timeout-interceptor.ts | 17 +++++++++++++---- .../src/internal/grpc/retry-interceptor.ts | 5 +++-- 3 files changed, 17 insertions(+), 7 deletions(-) diff --git a/packages/client-sdk-nodejs/src/config/retry/fixed-timeout-retry-strategy.ts b/packages/client-sdk-nodejs/src/config/retry/fixed-timeout-retry-strategy.ts index 9beaaebb4..c9c4a4aa4 100644 --- a/packages/client-sdk-nodejs/src/config/retry/fixed-timeout-retry-strategy.ts +++ b/packages/client-sdk-nodejs/src/config/retry/fixed-timeout-retry-strategy.ts @@ -45,7 +45,7 @@ export class FixedTimeoutRetryStrategy implements RetryStrategy { } this.logger.debug( - `Request is eligible for retry (attempt ${props.attemptNumber}), retrying soon.` + `Request is eligible for retry (attempt ${props.attemptNumber}), retrying after ${this.retryDelayIntervalMillis} +/- some jitter.` ); // retry after a fixed time interval has passed (+/- some jitter) return addJitter(this.retryDelayIntervalMillis); diff --git a/packages/client-sdk-nodejs/src/internal/grpc/client-timeout-interceptor.ts b/packages/client-sdk-nodejs/src/internal/grpc/client-timeout-interceptor.ts index fcc21aa9c..c173b61f3 100644 --- a/packages/client-sdk-nodejs/src/internal/grpc/client-timeout-interceptor.ts +++ b/packages/client-sdk-nodejs/src/internal/grpc/client-timeout-interceptor.ts @@ -79,7 +79,7 @@ class RetryUntilTimeoutInterceptor { receivedDeadline === this.overallDeadline ) { this.logger.debug( - 'Unable to successfully retry request within overall timeout, canceling request' + `Unable to successfully retry request within overall timeout of ${this.overallRequestTimeoutMs}ms, canceling request` ); // reset overall deadline for next request this.overallDeadline = undefined; @@ -87,7 +87,7 @@ class RetryUntilTimeoutInterceptor { const call = new InterceptingCall(nextCall(options)); call.cancelWithStatus( status.CANCELLED, - 'Unable to successfully retry request within overall timeout' + `Unable to successfully retry request within overall timeout of ${this.overallRequestTimeoutMs}ms` ); return call; } @@ -110,7 +110,14 @@ class RetryUntilTimeoutInterceptor { this.responseDataReceivedTimeoutMs ); this.logger.debug( - `Overall deadline set to ${this.overallDeadline.valueOf()}; incremental deadline set to ${options.deadline.valueOf()}` + `Overall deadline set to ${this.overallDeadline.valueOf()}, which is ${ + this.overallRequestTimeoutMs + }ms from now` + ); + this.logger.debug( + `Incremental deadline set to ${options.deadline.valueOf()}, which is ${ + this.responseDataReceivedTimeoutMs + }ms from now` ); return new InterceptingCall(nextCall(options)); } @@ -124,7 +131,9 @@ class RetryUntilTimeoutInterceptor { options.deadline = this.overallDeadline; } else { this.logger.debug( - `Incremental deadline set to ${newDeadline.valueOf()}` + `Incremental deadline set to ${newDeadline.valueOf()}, which is ${ + this.responseDataReceivedTimeoutMs + }ms from now` ); options.deadline = newDeadline; } diff --git a/packages/client-sdk-nodejs/src/internal/grpc/retry-interceptor.ts b/packages/client-sdk-nodejs/src/internal/grpc/retry-interceptor.ts index 69deb5c7c..83cc112da 100644 --- a/packages/client-sdk-nodejs/src/internal/grpc/retry-interceptor.ts +++ b/packages/client-sdk-nodejs/src/internal/grpc/retry-interceptor.ts @@ -66,9 +66,8 @@ export class RetryInterceptor { // eslint-disable-next-line @typescript-eslint/no-explicit-any next: (arg0: any) => void ) { - let attempts = 1; + let attempts = 0; const retry = function (message: unknown, metadata: Metadata) { - attempts++; const newCall = nextCall(options); newCall.start(metadata, { onReceiveMessage: function (message) { @@ -90,6 +89,7 @@ export class RetryInterceptor { savedMessageNext(savedReceiveMessage); next(status); } else { + attempts++; logger.debug( `Request eligible for retry: path: ${options.method_definition.path}; response status code: ${status.code}; number of attempts (${attempts}); will retry in ${whenToRetry}ms` ); @@ -117,6 +117,7 @@ export class RetryInterceptor { savedMessageNext(savedReceiveMessage); next(status); } else { + attempts++; logger.debug( `Request eligible for retry: path: ${options.method_definition.path}; response status code: ${status.code}; number of attempts (${attempts}); will retry in ${whenToRetry}ms` ); From e01feb98185db918bbecff1631ba03bf31e7f44d Mon Sep 17 00:00:00 2001 From: anitarua Date: Fri, 23 Aug 2024 14:40:22 -0700 Subject: [PATCH 37/43] make requestMetadata a required property --- .../src/config/retry/eligibility-strategy.ts | 2 +- packages/client-sdk-nodejs/src/config/retry/retry-strategy.ts | 2 +- .../client-sdk-nodejs/src/internal/grpc/retry-interceptor.ts | 1 + packages/client-sdk-nodejs/test/unit/storage-retry.test.ts | 4 ++++ 4 files changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/client-sdk-nodejs/src/config/retry/eligibility-strategy.ts b/packages/client-sdk-nodejs/src/config/retry/eligibility-strategy.ts index 20e41e456..865574b35 100644 --- a/packages/client-sdk-nodejs/src/config/retry/eligibility-strategy.ts +++ b/packages/client-sdk-nodejs/src/config/retry/eligibility-strategy.ts @@ -4,7 +4,7 @@ import {ClientMethodDefinition} from '@grpc/grpc-js/build/src/make-client'; export interface EligibleForRetryProps { grpcStatus: StatusObject; grpcRequest: ClientMethodDefinition; - requestMetadata?: Metadata; + requestMetadata: Metadata; } export interface EligibilityStrategy { diff --git a/packages/client-sdk-nodejs/src/config/retry/retry-strategy.ts b/packages/client-sdk-nodejs/src/config/retry/retry-strategy.ts index 1b641d528..ae2fe5a57 100644 --- a/packages/client-sdk-nodejs/src/config/retry/retry-strategy.ts +++ b/packages/client-sdk-nodejs/src/config/retry/retry-strategy.ts @@ -5,7 +5,7 @@ export interface DeterminewhenToRetryRequestProps { grpcStatus: StatusObject; grpcRequest: ClientMethodDefinition; attemptNumber: number; - requestMetadata?: Metadata; + requestMetadata: Metadata; } export interface RetryStrategy { diff --git a/packages/client-sdk-nodejs/src/internal/grpc/retry-interceptor.ts b/packages/client-sdk-nodejs/src/internal/grpc/retry-interceptor.ts index 83cc112da..45db20b45 100644 --- a/packages/client-sdk-nodejs/src/internal/grpc/retry-interceptor.ts +++ b/packages/client-sdk-nodejs/src/internal/grpc/retry-interceptor.ts @@ -109,6 +109,7 @@ export class RetryInterceptor { grpcStatus: status, grpcRequest: options.method_definition, attemptNumber: attempts, + requestMetadata: metadata, }); if (whenToRetry === null) { logger.debug( diff --git a/packages/client-sdk-nodejs/test/unit/storage-retry.test.ts b/packages/client-sdk-nodejs/test/unit/storage-retry.test.ts index 5d35018df..b4087f913 100644 --- a/packages/client-sdk-nodejs/test/unit/storage-retry.test.ts +++ b/packages/client-sdk-nodejs/test/unit/storage-retry.test.ts @@ -98,6 +98,7 @@ describe('storage configuration', () => { unknown >, attemptNumber: 1, + requestMetadata: new Metadata(), }); // should retry after 100ms +/- 10% jitter expect(whenToRetryDeadlineExceeded).toBeGreaterThanOrEqual(90); @@ -116,6 +117,7 @@ describe('storage configuration', () => { unknown >, attemptNumber: 1, + requestMetadata: new Metadata(), }); // should retry after 100ms +/- 10% jitter expect(whenToRetryInternalError).toBeGreaterThanOrEqual(90); @@ -134,6 +136,7 @@ describe('storage configuration', () => { unknown >, attemptNumber: 1, + requestMetadata: new Metadata(), }); // should retry after 100ms +/- 10% jitter expect(whenToRetryUnavailable).toBeGreaterThanOrEqual(90); @@ -152,6 +155,7 @@ describe('storage configuration', () => { unknown >, attemptNumber: 1, + requestMetadata: new Metadata(), }) ).toBeNull(); // will not retry }); From 5b69f57ee55482c388bc486864353def35be3d96 Mon Sep 17 00:00:00 2001 From: anitarua Date: Fri, 23 Aug 2024 14:54:11 -0700 Subject: [PATCH 38/43] fix storage examples file paths for npm target validate-examples --- examples/nodejs/storage/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/nodejs/storage/package.json b/examples/nodejs/storage/package.json index 4ce56a36c..e808d80e7 100644 --- a/examples/nodejs/storage/package.json +++ b/examples/nodejs/storage/package.json @@ -8,7 +8,7 @@ "build": "tsc", "basic": "tsc && node dist/basic.js", "doc-examples": "tsc && node dist/doc-examples-js-apis.js", - "validate-examples": "tsc && node dist/doc-examples-js-apis.js && node dist/cheat-sheet-main.js", + "validate-examples": "tsc && node dist/doc-example-files/doc-examples-js-apis.js && node dist/doc-example-files/cheat-sheet-main.js", "test": "jest", "lint": "eslint . --ext .ts", "format": "eslint . --ext .ts --fix" From 61981ecf5e99aaae083f2dbf4b6a167497bb5b19 Mon Sep 17 00:00:00 2001 From: Chris Price Date: Mon, 26 Aug 2024 12:47:15 -0700 Subject: [PATCH 39/43] chore: simplify header/retry interceptor construction, collapse timeout interceptor This commit does the following: * Simplify the logic for constructing header/retry interceptors; previously it involved instantiating a class that didn't really need to exist. These are now simpler factory functions with less state involved. * Collapse the timeout interceptor into the retry interceptor. Because the retry interceptor now needs to support overwriting the deadline for a given request in order to implement the more advanced type of retries that we want to support, it was starting to have overlaps/ conflicts with the ClientTimeoutInterceptor. In this commit we simply refactor the basic overall-request-timeout logic into the RetryInterceptor and update all of the call sites to use RetryInterceptor instead of the old ClientTimeoutInterceptor. There will still be another commit necessary in order to implement the intermediate timeouts that were implemented by `RetryUntilTimeoutInterceptor`, but as of this commit we should have parity with the previous support for retries and so integration tests should pass. client-timeout-interceptor.ts is left in place, commented out for now. Will be removed in a subsequent commit once the logic is ported into the retry interceptor. --- examples/nodejs/storage/basic.ts | 15 +- examples/nodejs/storage/package-lock.json | 819 ++---------------- examples/nodejs/storage/package.json | 2 +- .../src/auth-client-props.ts | 6 + .../src/config/auth-client-configuration.ts | 33 + .../src/config/auth-client-configurations.ts | 28 + .../src/config/retry/no-retry-strategy.ts | 27 + packages/client-sdk-nodejs/src/index.ts | 8 + .../src/internal/cache-control-client.ts | 11 +- .../src/internal/cache-data-client.ts | 19 +- .../grpc/client-timeout-interceptor.ts | 313 +++---- .../src/internal/grpc/headers-interceptor.ts | 27 +- .../src/internal/grpc/retry-interceptor.ts | 56 +- .../src/internal/internal-auth-client.ts | 14 +- .../src/internal/leaderboard-data-client.ts | 11 +- .../src/internal/ping-client.ts | 11 +- .../src/internal/pubsub-client.ts | 13 +- .../src/internal/storage-control-client.ts | 11 +- .../src/internal/storage-data-client.ts | 22 +- .../src/internal/webhook-client.ts | 11 +- 20 files changed, 460 insertions(+), 997 deletions(-) create mode 100644 packages/client-sdk-nodejs/src/config/auth-client-configuration.ts create mode 100644 packages/client-sdk-nodejs/src/config/auth-client-configurations.ts create mode 100644 packages/client-sdk-nodejs/src/config/retry/no-retry-strategy.ts diff --git a/examples/nodejs/storage/basic.ts b/examples/nodejs/storage/basic.ts index cf81d325f..d4f7352d9 100644 --- a/examples/nodejs/storage/basic.ts +++ b/examples/nodejs/storage/basic.ts @@ -1,16 +1,27 @@ import { CreateStoreResponse, CredentialProvider, + DefaultMomentoLoggerFactory, + DefaultMomentoLoggerLevel, PreviewStorageClient, StorageConfigurations, StorageGetResponse, StoragePutResponse, } from '@gomomento/sdk'; +import {FixedTimeoutRetryStrategy} from '@gomomento/sdk/dist/src/config/retry/fixed-timeout-retry-strategy'; async function main() { + const loggerFactory = new DefaultMomentoLoggerFactory(DefaultMomentoLoggerLevel.INFO); + + const retryStrategy = new FixedTimeoutRetryStrategy({ + loggerFactory: loggerFactory, + responseDataReceivedTimeoutMillis: 100, + retryDelayIntervalMillis: 1000, + }); + const storageClient = new PreviewStorageClient({ - configuration: StorageConfigurations.Laptop.latest(), - credentialProvider: CredentialProvider.fromEnvironmentVariable('MOMENTO_API_KEY'), + configuration: StorageConfigurations.Laptop.latest(loggerFactory).withRetryStrategy(retryStrategy), + credentialProvider: CredentialProvider.fromEnvironmentVariable('MOMENTO_API_KEY').withMomentoLocal(), }); const storeName = 'my-store'; diff --git a/examples/nodejs/storage/package-lock.json b/examples/nodejs/storage/package-lock.json index 7641e66a4..787c8bc06 100644 --- a/examples/nodejs/storage/package-lock.json +++ b/examples/nodejs/storage/package-lock.json @@ -9,7 +9,7 @@ "version": "1.0.0", "license": "ISC", "dependencies": { - "@gomomento/sdk": "^1.95.0" + "@gomomento/sdk": "file:../../../packages/client-sdk-nodejs" }, "devDependencies": { "@types/node": "16.11.4", @@ -27,6 +27,43 @@ "node": ">=10.4.0" } }, + "../../../packages/client-sdk-nodejs": { + "version": "0.0.1", + "license": "Apache-2.0", + "dependencies": { + "@gomomento/generated-types": "0.113.0", + "@gomomento/sdk-core": "file:../core", + "@grpc/grpc-js": "1.10.9", + "@types/google-protobuf": "3.15.10", + "google-protobuf": "3.21.2", + "jwt-decode": "3.1.2" + }, + "devDependencies": { + "@gomomento/common-integration-tests": "file:../common-integration-tests", + "@types/jest": "27.5.2", + "@types/node": "16.18.97", + "@types/uuid": "8.3.4", + "@typescript-eslint/eslint-plugin": "5.62.0", + "@typescript-eslint/parser": "5.62.0", + "eslint": "7.32.0", + "eslint-config-prettier": "8.10.0", + "eslint-plugin-import": "2.29.0", + "eslint-plugin-node": "11.1.0", + "eslint-plugin-prettier": "4.2.1", + "jest": "29.7.0", + "jest-extended": "4.0.2", + "jest-spec-reporter": "^1.0.19", + "prettier": "2.8.8", + "ts-jest": "29.1.1", + "ts-morph": "22.0.0", + "ts-node": "10.9.1", + "typescript": "4.9.5", + "uuid": "8.3.2" + }, + "engines": { + "node": ">= 16" + } + }, "node_modules/@eslint/eslintrc": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.4.1.tgz", @@ -50,73 +87,9 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/@gomomento/generated-types": { - "version": "0.113.0", - "resolved": "https://registry.npmjs.org/@gomomento/generated-types/-/generated-types-0.113.0.tgz", - "integrity": "sha512-7DJdcNWzCT5dpp1W+Vb77RZfamCt/SbtvoOcDtSDVU/wAsK7y7Yeo88DkgqiD2sCHv/u5X3wgLye51dlCsbgDw==", - "dependencies": { - "@grpc/grpc-js": "1.10.9", - "google-protobuf": "3.21.2", - "grpc-tools": "^1.12.4", - "protoc-gen-ts": "^0.8.6" - } - }, "node_modules/@gomomento/sdk": { - "version": "1.95.0", - "resolved": "https://registry.npmjs.org/@gomomento/sdk/-/sdk-1.95.0.tgz", - "integrity": "sha512-/Hx7Og1CivgrevmwM6iZr+ORW8isHhAfr2sFDGz8zKg9EZIsjpCdaeUEkR2dv402zfrsZvXPV3mjuYx7x5WAqg==", - "dependencies": { - "@gomomento/generated-types": "0.113.0", - "@gomomento/sdk-core": "1.95.0", - "@grpc/grpc-js": "1.10.9", - "@types/google-protobuf": "3.15.10", - "google-protobuf": "3.21.2", - "jwt-decode": "3.1.2" - }, - "engines": { - "node": ">= 16" - } - }, - "node_modules/@gomomento/sdk-core": { - "version": "1.95.0", - "resolved": "https://registry.npmjs.org/@gomomento/sdk-core/-/sdk-core-1.95.0.tgz", - "integrity": "sha512-1e3115v1AMk65HBUYisTnfoadTYefgMmrOJPLBYTIuyMDFh2JpxgQeSUnYN0tX5fn+0DJY2RBc9ujj9JzuNEeA==", - "dependencies": { - "buffer": "6.0.3", - "jwt-decode": "3.1.2" - }, - "engines": { - "node": ">= 16" - } - }, - "node_modules/@grpc/grpc-js": { - "version": "1.10.9", - "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.10.9.tgz", - "integrity": "sha512-5tcgUctCG0qoNyfChZifz2tJqbRbXVO9J7X6duFcOjY3HUNCxg5D0ZCK7EP9vIcZ0zRpLU9bWkyCqVCLZ46IbQ==", - "dependencies": { - "@grpc/proto-loader": "^0.7.13", - "@js-sdsl/ordered-map": "^4.4.2" - }, - "engines": { - "node": ">=12.10.0" - } - }, - "node_modules/@grpc/proto-loader": { - "version": "0.7.13", - "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.13.tgz", - "integrity": "sha512-AiXO/bfe9bmxBjxxtYxFAXGZvMaN5s8kO+jBHAJCON8rJoB5YS/D6X7ZNc6XQkuHNmyl4CYaMI1fJ/Gn27RGGw==", - "dependencies": { - "lodash.camelcase": "^4.3.0", - "long": "^5.0.0", - "protobufjs": "^7.2.5", - "yargs": "^17.7.2" - }, - "bin": { - "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" - }, - "engines": { - "node": ">=6" - } + "resolved": "../../../packages/client-sdk-nodejs", + "link": true }, "node_modules/@humanwhocodes/config-array": { "version": "0.9.5", @@ -138,34 +111,6 @@ "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", "dev": true }, - "node_modules/@js-sdsl/ordered-map": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz", - "integrity": "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/js-sdsl" - } - }, - "node_modules/@mapbox/node-pre-gyp": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz", - "integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==", - "dependencies": { - "detect-libc": "^2.0.0", - "https-proxy-agent": "^5.0.0", - "make-dir": "^3.1.0", - "node-fetch": "^2.6.7", - "nopt": "^5.0.0", - "npmlog": "^5.0.1", - "rimraf": "^3.0.2", - "semver": "^7.3.5", - "tar": "^6.1.11" - }, - "bin": { - "node-pre-gyp": "bin/node-pre-gyp" - } - }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -201,65 +146,6 @@ "node": ">= 8" } }, - "node_modules/@protobufjs/aspromise": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", - "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==" - }, - "node_modules/@protobufjs/base64": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", - "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==" - }, - "node_modules/@protobufjs/codegen": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", - "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==" - }, - "node_modules/@protobufjs/eventemitter": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", - "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==" - }, - "node_modules/@protobufjs/fetch": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", - "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", - "dependencies": { - "@protobufjs/aspromise": "^1.1.1", - "@protobufjs/inquire": "^1.1.0" - } - }, - "node_modules/@protobufjs/float": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", - "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==" - }, - "node_modules/@protobufjs/inquire": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", - "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==" - }, - "node_modules/@protobufjs/path": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", - "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==" - }, - "node_modules/@protobufjs/pool": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", - "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==" - }, - "node_modules/@protobufjs/utf8": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", - "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==" - }, - "node_modules/@types/google-protobuf": { - "version": "3.15.10", - "resolved": "https://registry.npmjs.org/@types/google-protobuf/-/google-protobuf-3.15.10.tgz", - "integrity": "sha512-uiyKJCa8hbmPE4yxwjbkMOALaBAiOVcatW/yEGbjTqwAh4kzNgQPWRlJMNPXpB5CPUM66xsYufiSX9WKHZCE9g==" - }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -275,7 +161,8 @@ "node_modules/@types/node": { "version": "16.11.4", "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.4.tgz", - "integrity": "sha512-TMgXmy0v2xWyuCSCJM6NCna2snndD8yvQF67J29ipdzMcsPa9u+o0tjF5+EQNdhcuZplYuouYqpc4zcd5I6amQ==" + "integrity": "sha512-TMgXmy0v2xWyuCSCJM6NCna2snndD8yvQF67J29ipdzMcsPa9u+o0tjF5+EQNdhcuZplYuouYqpc4zcd5I6amQ==", + "dev": true }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "5.30.5", @@ -578,11 +465,6 @@ "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/abbrev": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" - }, "node_modules/acorn": { "version": "8.11.3", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", @@ -604,17 +486,6 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, - "node_modules/agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", - "dependencies": { - "debug": "4" - }, - "engines": { - "node": ">= 6.0.0" - } - }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -635,6 +506,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, "engines": { "node": ">=8" } @@ -643,6 +515,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -653,24 +526,6 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/aproba": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", - "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==" - }, - "node_modules/are-we-there-yet": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", - "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", - "deprecated": "This package is no longer supported.", - "dependencies": { - "delegates": "^1.0.0", - "readable-stream": "^3.6.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -780,31 +635,14 @@ "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" - }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -822,29 +660,6 @@ "node": ">=8" } }, - "node_modules/buffer": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", - "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.2.1" - } - }, "node_modules/call-bind": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", @@ -889,31 +704,11 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/chownr": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", - "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", - "engines": { - "node": ">=10" - } - }, - "node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, "dependencies": { "color-name": "~1.1.4" }, @@ -924,25 +719,14 @@ "node_modules/color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, - "node_modules/color-support": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", - "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", - "bin": { - "color-support": "bin.js" - } + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" - }, - "node_modules/console-control-strings": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", - "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==" + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true }, "node_modules/cross-spawn": { "version": "7.0.3", @@ -1013,6 +797,7 @@ "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, "dependencies": { "ms": "2.1.2" }, @@ -1065,19 +850,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/delegates": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", - "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==" - }, - "node_modules/detect-libc": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", - "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", - "engines": { - "node": ">=8" - } - }, "node_modules/dir-glob": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", @@ -1102,11 +874,6 @@ "node": ">=6.0.0" } }, - "node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" - }, "node_modules/es-abstract": { "version": "1.23.3", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.3.tgz", @@ -1240,14 +1007,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/escalade": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", - "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", - "engines": { - "node": ">=6" - } - }, "node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -1809,32 +1568,11 @@ "is-callable": "^1.1.3" } }, - "node_modules/fs-minipass": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", - "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/fs-minipass/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true }, "node_modules/function-bind": { "version": "1.1.2", @@ -1878,34 +1616,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/gauge": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", - "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", - "deprecated": "This package is no longer supported.", - "dependencies": { - "aproba": "^1.0.3 || ^2.0.0", - "color-support": "^1.1.2", - "console-control-strings": "^1.0.0", - "has-unicode": "^2.0.1", - "object-assign": "^4.1.1", - "signal-exit": "^3.0.0", - "string-width": "^4.2.3", - "strip-ansi": "^6.0.1", - "wide-align": "^1.1.2" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "engines": { - "node": "6.* || 8.* || >= 10.*" - } - }, "node_modules/get-intrinsic": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", @@ -1947,6 +1657,7 @@ "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -2025,11 +1736,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/google-protobuf": { - "version": "3.21.2", - "resolved": "https://registry.npmjs.org/google-protobuf/-/google-protobuf-3.21.2.tgz", - "integrity": "sha512-3MSOYFO5U9mPGikIYCzK0SaThypfGgS6bHqrUGXG3DPHCrb+txNqeEcns1W0lkGfk0rCyNXm7xB9rMxnCiZOoA==" - }, "node_modules/gopd": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", @@ -2042,19 +1748,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/grpc-tools": { - "version": "1.12.4", - "resolved": "https://registry.npmjs.org/grpc-tools/-/grpc-tools-1.12.4.tgz", - "integrity": "sha512-5+mLAJJma3BjnW/KQp6JBjUMgvu7Mu3dBvBPd1dcbNIb+qiR0817zDpgPjS7gRb+l/8EVNIa3cB02xI9JLToKg==", - "hasInstallScript": true, - "dependencies": { - "@mapbox/node-pre-gyp": "^1.0.5" - }, - "bin": { - "grpc_tools_node_protoc": "bin/protoc.js", - "grpc_tools_node_protoc_plugin": "bin/protoc_plugin.js" - } - }, "node_modules/has": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/has/-/has-1.0.4.tgz", @@ -2133,11 +1826,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/has-unicode": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", - "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==" - }, "node_modules/hasown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", @@ -2150,37 +1838,6 @@ "node": ">= 0.4" } }, - "node_modules/https-proxy-agent": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", - "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", - "dependencies": { - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, "node_modules/ignore": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", @@ -2220,6 +1877,7 @@ "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, "dependencies": { "once": "^1.3.0", "wrappy": "1" @@ -2228,7 +1886,8 @@ "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true }, "node_modules/internal-slot": { "version": "1.0.7", @@ -2351,14 +2010,6 @@ "node": ">=0.10.0" } }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "engines": { - "node": ">=8" - } - }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -2549,11 +2200,6 @@ "json5": "lib/cli.js" } }, - "node_modules/jwt-decode": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-3.1.2.tgz", - "integrity": "sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A==" - }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -2576,44 +2222,12 @@ "node": ">= 0.8.0" } }, - "node_modules/lodash.camelcase": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", - "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==" - }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, - "node_modules/long": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz", - "integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==" - }, - "node_modules/make-dir": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", - "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", - "dependencies": { - "semver": "^6.0.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/make-dir/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "bin": { - "semver": "bin/semver.js" - } - }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -2640,6 +2254,7 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, "dependencies": { "brace-expansion": "^1.1.7" }, @@ -2656,52 +2271,11 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/minipass": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", - "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", - "engines": { - "node": ">=8" - } - }, - "node_modules/minizlib": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", - "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", - "dependencies": { - "minipass": "^3.0.0", - "yallist": "^4.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/minizlib/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", - "bin": { - "mkdirp": "bin/cmd.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true }, "node_modules/natural-compare": { "version": "1.4.0", @@ -2709,59 +2283,6 @@ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true }, - "node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, - "node_modules/nopt": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", - "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", - "dependencies": { - "abbrev": "1" - }, - "bin": { - "nopt": "bin/nopt.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/npmlog": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", - "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", - "deprecated": "This package is no longer supported.", - "dependencies": { - "are-we-there-yet": "^2.0.0", - "console-control-strings": "^1.1.0", - "gauge": "^3.0.0", - "set-blocking": "^2.0.0" - } - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/object-inspect": { "version": "1.13.1", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", @@ -2819,6 +2340,7 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, "dependencies": { "wrappy": "1" } @@ -2856,6 +2378,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, "engines": { "node": ">=0.10.0" } @@ -2941,41 +2464,6 @@ "node": ">=6.0.0" } }, - "node_modules/protobufjs": { - "version": "7.3.2", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.3.2.tgz", - "integrity": "sha512-RXyHaACeqXeqAKGLDl68rQKbmObRsTIn4TYVUUug1KfS47YWCo5MacGITEryugIgZqORCvJWEk4l449POg5Txg==", - "hasInstallScript": true, - "dependencies": { - "@protobufjs/aspromise": "^1.1.2", - "@protobufjs/base64": "^1.1.2", - "@protobufjs/codegen": "^2.0.4", - "@protobufjs/eventemitter": "^1.1.0", - "@protobufjs/fetch": "^1.1.0", - "@protobufjs/float": "^1.0.2", - "@protobufjs/inquire": "^1.1.0", - "@protobufjs/path": "^1.1.2", - "@protobufjs/pool": "^1.1.0", - "@protobufjs/utf8": "^1.1.0", - "@types/node": ">=13.7.0", - "long": "^5.0.0" - }, - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/protoc-gen-ts": { - "version": "0.8.7", - "resolved": "https://registry.npmjs.org/protoc-gen-ts/-/protoc-gen-ts-0.8.7.tgz", - "integrity": "sha512-jr4VJey2J9LVYCV7EVyVe53g1VMw28cCmYJhBe5e3YX5wiyiDwgxWxeDf9oTqAe4P1bN/YGAkW2jhlH8LohwiQ==", - "bin": { - "protoc-gen-ts": "protoc-gen-ts.js" - }, - "funding": { - "type": "individual", - "url": "https://www.buymeacoffee.com/thesayyn" - } - }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -3005,19 +2493,6 @@ } ] }, - "node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/regexp.prototype.flags": { "version": "1.5.2", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz", @@ -3048,14 +2523,6 @@ "url": "https://github.com/sponsors/mysticatea" } }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/resolve": { "version": "1.22.8", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", @@ -3097,6 +2564,7 @@ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, "dependencies": { "glob": "^7.1.3" }, @@ -3148,25 +2616,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, "node_modules/safe-regex-test": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.3.tgz", @@ -3188,6 +2637,7 @@ "version": "7.6.2", "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "dev": true, "bin": { "semver": "bin/semver.js" }, @@ -3195,11 +2645,6 @@ "node": ">=10" } }, - "node_modules/set-blocking": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==" - }, "node_modules/set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", @@ -3271,11 +2716,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" - }, "node_modules/slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", @@ -3285,27 +2725,6 @@ "node": ">=8" } }, - "node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, - "node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/string.prototype.trim": { "version": "1.2.9", "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.9.tgz", @@ -3359,6 +2778,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, "dependencies": { "ansi-regex": "^5.0.1" }, @@ -3411,22 +2831,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/tar": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", - "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", - "dependencies": { - "chownr": "^2.0.0", - "fs-minipass": "^2.0.0", - "minipass": "^5.0.0", - "minizlib": "^2.1.1", - "mkdirp": "^1.0.3", - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -3445,11 +2849,6 @@ "node": ">=8.0" } }, - "node_modules/tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" - }, "node_modules/tsconfig-paths": { "version": "3.15.0", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", @@ -3617,31 +3016,12 @@ "punycode": "^2.1.0" } }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" - }, "node_modules/v8-compile-cache": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.4.0.tgz", "integrity": "sha512-ocyWc3bAHBB/guyqJQVI5o4BZkPhznPYUG2ea80Gond/BgNWpap8TOmLSeeQG7bnh2KMISxskdADG59j7zruhw==", "dev": true }, - "node_modules/webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" - }, - "node_modules/whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "dependencies": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } - }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -3692,14 +3072,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/wide-align": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", - "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", - "dependencies": { - "string-width": "^1.0.2 || 2 || 3 || 4" - } - }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", @@ -3709,64 +3081,11 @@ "node": ">=0.10.0" } }, - "node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" - }, - "node_modules/y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "engines": { - "node": ">=10" - } - }, - "node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" - }, - "node_modules/yargs": { - "version": "17.7.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", - "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", - "dependencies": { - "cliui": "^8.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", - "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "engines": { - "node": ">=12" - } + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true } } } diff --git a/examples/nodejs/storage/package.json b/examples/nodejs/storage/package.json index e808d80e7..927a2a40c 100644 --- a/examples/nodejs/storage/package.json +++ b/examples/nodejs/storage/package.json @@ -28,7 +28,7 @@ "typescript": "4.9.5" }, "dependencies": { - "@gomomento/sdk": "^1.95.0" + "@gomomento/sdk": "file:../../../packages/client-sdk-nodejs" }, "engines": { "node": ">=10.4.0" diff --git a/packages/client-sdk-nodejs/src/auth-client-props.ts b/packages/client-sdk-nodejs/src/auth-client-props.ts index c1f71ac04..37d29aca9 100644 --- a/packages/client-sdk-nodejs/src/auth-client-props.ts +++ b/packages/client-sdk-nodejs/src/auth-client-props.ts @@ -1,4 +1,5 @@ import {CredentialProvider} from '.'; +import {AuthClientConfiguration} from './config/auth-client-configuration'; export interface AuthClientProps { /** @@ -6,6 +7,11 @@ export interface AuthClientProps { */ credentialProvider: CredentialProvider; + /** + * Controls the configuration settings for the auth client, such as logging configuration. + */ + configuration?: AuthClientConfiguration; + /** * Configures whether the client should return a Momento Error object or throw an exception when an * error occurs. By default, this is set to false, and the client will return a Momento Error object on errors. Set it diff --git a/packages/client-sdk-nodejs/src/config/auth-client-configuration.ts b/packages/client-sdk-nodejs/src/config/auth-client-configuration.ts new file mode 100644 index 000000000..3ea204120 --- /dev/null +++ b/packages/client-sdk-nodejs/src/config/auth-client-configuration.ts @@ -0,0 +1,33 @@ +import {MomentoLoggerFactory} from '@gomomento/sdk-core'; + +export interface AuthClientConfigurationProps { + /** + * Configures logging verbosity and format + */ + loggerFactory: MomentoLoggerFactory; +} + +/** + * Configuration options for Momento CacheClient. + * + * @export + * @interface Configuration + */ +export interface AuthConfiguration { + /** + * @returns {MomentoLoggerFactory} the current configuration options for logging verbosity and format + */ + getLoggerFactory(): MomentoLoggerFactory; +} + +export class AuthClientConfiguration implements AuthConfiguration { + private readonly loggerFactory: MomentoLoggerFactory; + + constructor(props: AuthClientConfigurationProps) { + this.loggerFactory = props.loggerFactory; + } + + getLoggerFactory(): MomentoLoggerFactory { + return this.loggerFactory; + } +} diff --git a/packages/client-sdk-nodejs/src/config/auth-client-configurations.ts b/packages/client-sdk-nodejs/src/config/auth-client-configurations.ts new file mode 100644 index 000000000..a42ef3ac4 --- /dev/null +++ b/packages/client-sdk-nodejs/src/config/auth-client-configurations.ts @@ -0,0 +1,28 @@ +import {AuthClientConfiguration} from './auth-client-configuration'; +import {MomentoLoggerFactory} from '@gomomento/sdk-core'; +import {DefaultMomentoLoggerFactory} from './logging/default-momento-logger'; + +const defaultLoggerFactory: MomentoLoggerFactory = + new DefaultMomentoLoggerFactory(); + +/** + * Laptop config provides defaults suitable for a medium-to-high-latency dev environment. Permissive timeouts, retries, and + * relaxed latency and throughput targets. + * @export + * @class Laptop + */ +export class Default extends AuthClientConfiguration { + /** + * Provides the latest recommended configuration for a laptop development environment. NOTE: this configuration may + * change in future releases to take advantage of improvements we identify for default configurations. + * @param {MomentoLoggerFactory} [loggerFactory=defaultLoggerFactory] + * @returns {CacheConfiguration} + */ + static latest( + loggerFactory: MomentoLoggerFactory = defaultLoggerFactory + ): AuthClientConfiguration { + return new AuthClientConfiguration({ + loggerFactory: loggerFactory, + }); + } +} diff --git a/packages/client-sdk-nodejs/src/config/retry/no-retry-strategy.ts b/packages/client-sdk-nodejs/src/config/retry/no-retry-strategy.ts new file mode 100644 index 000000000..355a1f000 --- /dev/null +++ b/packages/client-sdk-nodejs/src/config/retry/no-retry-strategy.ts @@ -0,0 +1,27 @@ +import { + DeterminewhenToRetryRequestProps, + RetryStrategy, +} from './retry-strategy'; +import {MomentoLoggerFactory, MomentoLogger} from '../../'; + +export interface NoRetryStrategyProps { + loggerFactory: MomentoLoggerFactory; +} + +export class NoRetryStrategy implements RetryStrategy { + private readonly logger: MomentoLogger; + + constructor(props: NoRetryStrategyProps) { + this.logger = props.loggerFactory.getLogger(this); + } + + determineWhenToRetryRequest( + props: DeterminewhenToRetryRequestProps + ): number | null { + this.logger.debug( + `Using no-retry strategy, therefore not retrying request; status code: ${props.grpcStatus.code}, request type: ${props.grpcRequest.path}, attemptNumber: ${props.attemptNumber}` + ); + // null means do not retry + return null; + } +} diff --git a/packages/client-sdk-nodejs/src/index.ts b/packages/client-sdk-nodejs/src/index.ts index 1d758ad35..25e7dec2f 100644 --- a/packages/client-sdk-nodejs/src/index.ts +++ b/packages/client-sdk-nodejs/src/index.ts @@ -2,6 +2,7 @@ import {CacheClient, SimpleCacheClient} from './cache-client'; import {TopicClient} from './topic-client'; import {PreviewStorageClient} from './preview-storage-client'; import * as Configurations from './config/configurations'; +import * as AuthClientConfigurations from './config/auth-client-configurations'; import * as TopicConfigurations from './config/topic-configurations'; import * as StorageConfigurations from './config/storage-configurations'; import * as LeaderboardConfigurations from './config/leaderboard-configurations'; @@ -179,6 +180,10 @@ import { } from '@gomomento/sdk-core'; import {Configuration, CacheConfiguration} from './config/configuration'; +import { + AuthConfiguration, + AuthClientConfiguration, +} from './config/auth-client-configuration'; import { TopicConfiguration, TopicClientConfiguration, @@ -417,6 +422,9 @@ export { WebhookDestinationType, // AuthClient response types AuthClient, + AuthConfiguration, + AuthClientConfiguration, + AuthClientConfigurations, GenerateApiKey, /** * @deprecated Use 'GenerateApiKey' instead diff --git a/packages/client-sdk-nodejs/src/internal/cache-control-client.ts b/packages/client-sdk-nodejs/src/internal/cache-control-client.ts index d64d5d002..8cfc21ee7 100644 --- a/packages/client-sdk-nodejs/src/internal/cache-control-client.ts +++ b/packages/client-sdk-nodejs/src/internal/cache-control-client.ts @@ -1,7 +1,6 @@ import {control} from '@gomomento/generated-types'; import grpcControl = control.control_client; -import {Header, HeaderInterceptorProvider} from './grpc/headers-interceptor'; -import {ClientTimeoutInterceptor} from './grpc/client-timeout-interceptor'; +import {Header, HeaderInterceptor} from './grpc/headers-interceptor'; import {CacheServiceErrorMapper} from '../errors/cache-service-error-mapper'; import {ChannelCredentials, Interceptor} from '@grpc/grpc-js'; import { @@ -23,6 +22,7 @@ import { CacheLimits, TopicLimits, } from '@gomomento/sdk-core/dist/src/messages/cache-info'; +import {RetryInterceptor} from './grpc/retry-interceptor'; export interface ControlClientProps { configuration: Configuration; @@ -50,8 +50,11 @@ export class CacheControlClient { new Header('runtime-version', `nodejs:${process.versions.node}`), ]; this.interceptors = [ - new HeaderInterceptorProvider(headers).createHeadersInterceptor(), - ClientTimeoutInterceptor(CacheControlClient.REQUEST_TIMEOUT_MS), + HeaderInterceptor.createHeadersInterceptor(headers), + RetryInterceptor.createRetryInterceptor({ + loggerFactory: props.configuration.getLoggerFactory(), + overallRequestTimeoutMs: CacheControlClient.REQUEST_TIMEOUT_MS, + }), ]; this.logger.debug( `Creating control client using endpoint: '${props.credentialProvider.getControlEndpoint()}` diff --git a/packages/client-sdk-nodejs/src/internal/cache-data-client.ts b/packages/client-sdk-nodejs/src/internal/cache-data-client.ts index b04cc9834..6bed3360f 100644 --- a/packages/client-sdk-nodejs/src/internal/cache-data-client.ts +++ b/packages/client-sdk-nodejs/src/internal/cache-data-client.ts @@ -1,9 +1,8 @@ import {cache} from '@gomomento/generated-types'; // older versions of node don't have the global util variables https://github.com/nodejs/node/issues/20365 import {TextEncoder} from 'util'; -import {Header, HeaderInterceptorProvider} from './grpc/headers-interceptor'; -import {ClientTimeoutInterceptor} from './grpc/client-timeout-interceptor'; -import {createRetryInterceptorIfEnabled} from './grpc/retry-interceptor'; +import {Header, HeaderInterceptor} from './grpc/headers-interceptor'; +import {RetryInterceptor} from './grpc/retry-interceptor'; import {CacheServiceErrorMapper} from '../errors/cache-service-error-mapper'; import { ChannelCredentials, @@ -4120,19 +4119,19 @@ export class CacheDataClient implements IDataClient { middlewareRequestContext, this.clientWrapper.getClient() ), - new HeaderInterceptorProvider(headers).createHeadersInterceptor(), - ClientTimeoutInterceptor(this.requestTimeoutMs), - ...createRetryInterceptorIfEnabled( - this.configuration.getLoggerFactory(), - this.configuration.getRetryStrategy() - ), + HeaderInterceptor.createHeadersInterceptor(headers), + RetryInterceptor.createRetryInterceptor({ + loggerFactory: this.configuration.getLoggerFactory(), + retryStrategy: this.configuration.getRetryStrategy(), + overallRequestTimeoutMs: this.requestTimeoutMs, + }), ]; } // TODO https://github.com/momentohq/client-sdk-nodejs/issues/349 // decide on streaming interceptors and middlewares private initializeStreamingInterceptors(headers: Header[]): Interceptor[] { - return [new HeaderInterceptorProvider(headers).createHeadersInterceptor()]; + return [HeaderInterceptor.createHeadersInterceptor(headers)]; } private convert(v: string | Uint8Array): Uint8Array { diff --git a/packages/client-sdk-nodejs/src/internal/grpc/client-timeout-interceptor.ts b/packages/client-sdk-nodejs/src/internal/grpc/client-timeout-interceptor.ts index c173b61f3..bbb2b2caf 100644 --- a/packages/client-sdk-nodejs/src/internal/grpc/client-timeout-interceptor.ts +++ b/packages/client-sdk-nodejs/src/internal/grpc/client-timeout-interceptor.ts @@ -1,151 +1,162 @@ -import {MomentoLogger, MomentoLoggerFactory} from '@gomomento/sdk-core'; -import {InterceptingCall, Interceptor, status} from '@grpc/grpc-js'; -import {RetryStrategy} from '../../config/retry/retry-strategy'; -import {DefaultMomentoLoggerFactory} from '../../config/logging/default-momento-logger'; - -// Determine which retry strategy is specified in the configuration -// and which interceptor to use. -export const ClientTimeoutInterceptor = ( - overallRequestTimeoutMs: number, - retryStrategy?: RetryStrategy, - loggerFactory?: MomentoLoggerFactory -): Interceptor => { - if ( - retryStrategy !== undefined && - retryStrategy.responseDataReceivedTimeoutMillis !== undefined - ) { - return new RetryUntilTimeoutInterceptor( - loggerFactory ?? new DefaultMomentoLoggerFactory(), - overallRequestTimeoutMs, - retryStrategy.responseDataReceivedTimeoutMillis - ).createTimeoutInterceptor(); - } - return new BasicTimeoutInterceptor( - overallRequestTimeoutMs - ).createTimeoutInterceptor(); -}; - -class BasicTimeoutInterceptor { - private readonly overallRequestTimeoutMs: number; - - constructor(overallRequestTimeoutMs: number) { - this.overallRequestTimeoutMs = overallRequestTimeoutMs; - } - - public createTimeoutInterceptor(): Interceptor { - return (options, nextCall) => { - if (!options.deadline) { - const deadline = new Date(Date.now()); - deadline.setMilliseconds( - deadline.getMilliseconds() + this.overallRequestTimeoutMs - ); - options.deadline = deadline; - } - return new InterceptingCall(nextCall(options)); - }; - } -} - -class RetryUntilTimeoutInterceptor { - private readonly logger: MomentoLogger; - private readonly responseDataReceivedTimeoutMs: number; - private readonly overallRequestTimeoutMs: number; - private overallDeadline?: Date; - private previousRpc?: string; - - constructor( - loggerFactory: MomentoLoggerFactory, - overallRequestTimeoutMs: number, - responseDataReceivedTimeoutMs: number - ) { - this.logger = loggerFactory.getLogger(this); - this.responseDataReceivedTimeoutMs = responseDataReceivedTimeoutMs; - this.overallRequestTimeoutMs = overallRequestTimeoutMs; - } - - public createTimeoutInterceptor(): Interceptor { - return (options, nextCall) => { - this.logger.debug( - `Previous RPC: ${this.previousRpc ?? 'none'} | Incoming RPC: ${ - options.method_definition.path - }` - ); - - // If the received deadline is equal to the overall deadline, we've - // maxed out the retries on a particular request and should cancel retries. - const receivedDeadline = options.deadline; - if ( - receivedDeadline !== undefined && - receivedDeadline === this.overallDeadline - ) { - this.logger.debug( - `Unable to successfully retry request within overall timeout of ${this.overallRequestTimeoutMs}ms, canceling request` - ); - // reset overall deadline for next request - this.overallDeadline = undefined; - this.previousRpc = undefined; - const call = new InterceptingCall(nextCall(options)); - call.cancelWithStatus( - status.CANCELLED, - `Unable to successfully retry request within overall timeout of ${this.overallRequestTimeoutMs}ms` - ); - return call; - } - - // Reset overall and incremental deadlines in these cases: - // Case 1: overallDeadline is undefined (first request or previous was canceled) - // Case 2: different RPC path requested through same client - // Case 3: new request through same client after original retry deadline has passed - // (Note: Case 3 check must occur after the receivedDeadline == options.deadline check - // or else the request is always retried). - if ( - this.overallDeadline === undefined || - this.previousRpc !== options.method_definition.path || - (this.overallDeadline !== undefined && - Date.now().valueOf() > this.overallDeadline.valueOf()) - ) { - this.previousRpc = options.method_definition.path; - this.overallDeadline = createNewDeadline(this.overallRequestTimeoutMs); - options.deadline = createNewDeadline( - this.responseDataReceivedTimeoutMs - ); - this.logger.debug( - `Overall deadline set to ${this.overallDeadline.valueOf()}, which is ${ - this.overallRequestTimeoutMs - }ms from now` - ); - this.logger.debug( - `Incremental deadline set to ${options.deadline.valueOf()}, which is ${ - this.responseDataReceivedTimeoutMs - }ms from now` - ); - return new InterceptingCall(nextCall(options)); - } - - // Otherwise, we've hit an incremental timeout and must set the next deadline. - const newDeadline = createNewDeadline(this.responseDataReceivedTimeoutMs); - if (newDeadline > this.overallDeadline) { - this.logger.debug( - `New incremental deadline ${newDeadline.valueOf()} would exceed overall deadline, setting to overall deadline ${this.overallDeadline.valueOf()}` - ); - options.deadline = this.overallDeadline; - } else { - this.logger.debug( - `Incremental deadline set to ${newDeadline.valueOf()}, which is ${ - this.responseDataReceivedTimeoutMs - }ms from now` - ); - options.deadline = newDeadline; - } - this.previousRpc = options.method_definition.path; - - return new InterceptingCall(nextCall(options)); - }; - } -} - -function createNewDeadline(timeToAddMillis: number): Date { - const deadline = new Date(Date.now()); - deadline.setMilliseconds(deadline.getMilliseconds() + timeToAddMillis); - return deadline; -} +// import {MomentoLogger, MomentoLoggerFactory} from '@gomomento/sdk-core'; +// import {InterceptingCall, Interceptor, status} from '@grpc/grpc-js'; +// import {RetryStrategy} from '../../config/retry/retry-strategy'; +// import {DefaultMomentoLoggerFactory} from '../../config/logging/default-momento-logger'; +// +// // Determine which retry strategy is specified in the configuration +// // and which interceptor to use. +// export const ClientTimeoutInterceptor = ( +// overallRequestTimeoutMs: number, +// retryStrategy?: RetryStrategy, +// loggerFactory?: MomentoLoggerFactory +// ): Interceptor => { +// if ( +// retryStrategy !== undefined && +// retryStrategy.responseDataReceivedTimeoutMillis !== undefined +// ) { +// return new RetryUntilTimeoutInterceptor( +// loggerFactory ?? new DefaultMomentoLoggerFactory(), +// overallRequestTimeoutMs, +// retryStrategy.responseDataReceivedTimeoutMillis +// ).createTimeoutInterceptor(); +// } +// return new BasicTimeoutInterceptor( +// overallRequestTimeoutMs +// ).createTimeoutInterceptor(); +// }; +// +// class BasicTimeoutInterceptor { +// private readonly overallRequestTimeoutMs: number; +// +// constructor(overallRequestTimeoutMs: number) { +// this.overallRequestTimeoutMs = overallRequestTimeoutMs; +// } +// +// public createTimeoutInterceptor(): Interceptor { +// return (options, nextCall) => { +// if (!options.deadline) { +// const deadline = new Date(Date.now()); +// deadline.setMilliseconds( +// deadline.getMilliseconds() + this.overallRequestTimeoutMs +// ); +// options.deadline = deadline; +// } +// return new InterceptingCall(nextCall(options)); +// }; +// } +// } +// +// class RetryUntilTimeoutInterceptor { +// private readonly logger: MomentoLogger; +// private readonly responseDataReceivedTimeoutMs: number; +// private readonly overallRequestTimeoutMs: number; +// private overallDeadline?: Date; +// private previousRpc?: string; +// +// constructor( +// loggerFactory: MomentoLoggerFactory, +// overallRequestTimeoutMs: number, +// responseDataReceivedTimeoutMs: number +// ) { +// this.logger = loggerFactory.getLogger(this); +// this.responseDataReceivedTimeoutMs = responseDataReceivedTimeoutMs; +// this.overallRequestTimeoutMs = overallRequestTimeoutMs; +// } +// +// public createTimeoutInterceptor(): Interceptor { +// return (options, nextCall) => { +// this.logger.debug( +// `Previous RPC: ${this.previousRpc ?? 'none'} | Incoming RPC: ${ +// options.method_definition.path +// }` +// ); +// +// console.log('The client timeout interceptor has been called'); +// +// // If the received deadline is equal to the overall deadline, we've +// // maxed out the retries on a particular request and should cancel retries. +// const receivedDeadline = options.deadline; +// if ( +// receivedDeadline !== undefined && +// receivedDeadline === this.overallDeadline +// ) { +// this.logger.debug( +// `Unable to successfully retry request within overall timeout of ${this.overallRequestTimeoutMs}ms, canceling request` +// ); +// // reset overall deadline for next request +// this.overallDeadline = undefined; +// this.previousRpc = undefined; +// const call = new InterceptingCall(nextCall(options)); +// console.log( +// 'The client timeout interceptor is returning; detected timeout' +// ); +// call.cancelWithStatus( +// status.CANCELLED, +// `Unable to successfully retry request within overall timeout of ${this.overallRequestTimeoutMs}ms` +// ); +// return call; +// } +// +// // Reset overall and incremental deadlines in these cases: +// // Case 1: overallDeadline is undefined (first request or previous was canceled) +// // Case 2: different RPC path requested through same client +// // Case 3: new request through same client after original retry deadline has passed +// // (Note: Case 3 check must occur after the receivedDeadline == options.deadline check +// // or else the request is always retried). +// if ( +// this.overallDeadline === undefined || +// this.previousRpc !== options.method_definition.path || +// (this.overallDeadline !== undefined && +// Date.now().valueOf() > this.overallDeadline.valueOf()) +// ) { +// this.previousRpc = options.method_definition.path; +// this.overallDeadline = createNewDeadline(this.overallRequestTimeoutMs); +// options.deadline = createNewDeadline( +// this.responseDataReceivedTimeoutMs +// ); +// this.logger.debug( +// `Overall deadline set to ${this.overallDeadline.valueOf()}, which is ${ +// this.overallRequestTimeoutMs +// }ms from now` +// ); +// this.logger.debug( +// `Incremental deadline set to ${options.deadline.valueOf()}, which is ${ +// this.responseDataReceivedTimeoutMs +// }ms from now` +// ); +// console.log( +// 'The client timeout interceptor is returning; reset overall and incremental deadline' +// ); +// return new InterceptingCall(nextCall(options)); +// } +// +// // Otherwise, we've hit an incremental timeout and must set the next deadline. +// const newDeadline = createNewDeadline(this.responseDataReceivedTimeoutMs); +// if (newDeadline > this.overallDeadline) { +// this.logger.debug( +// `New incremental deadline ${newDeadline.valueOf()} would exceed overall deadline, setting to overall deadline ${this.overallDeadline.valueOf()}` +// ); +// options.deadline = this.overallDeadline; +// } else { +// this.logger.debug( +// `Incremental deadline set to ${newDeadline.valueOf()}, which is ${ +// this.responseDataReceivedTimeoutMs +// }ms from now` +// ); +// options.deadline = newDeadline; +// } +// this.previousRpc = options.method_definition.path; +// +// console.log( +// 'The client timeout interceptor is returning; detected incremental timeout' +// ); +// return new InterceptingCall(nextCall(options)); +// }; +// } +// } +// +// function createNewDeadline(timeToAddMillis: number): Date { +// const deadline = new Date(Date.now()); +// deadline.setMilliseconds(deadline.getMilliseconds() + timeToAddMillis); +// return deadline; +// } diff --git a/packages/client-sdk-nodejs/src/internal/grpc/headers-interceptor.ts b/packages/client-sdk-nodejs/src/internal/grpc/headers-interceptor.ts index 2658ed245..6b931ffec 100644 --- a/packages/client-sdk-nodejs/src/internal/grpc/headers-interceptor.ts +++ b/packages/client-sdk-nodejs/src/internal/grpc/headers-interceptor.ts @@ -15,33 +15,24 @@ export class Header { } } -export class HeaderInterceptorProvider { - private readonly headersToAddEveryTime: Header[]; - private readonly headersToAddOnce: Header[]; - private areOnlyOnceHeadersSent = false; - - /** - * @param {Header[]} headers - */ - constructor(headers: Header[]) { - this.headersToAddOnce = headers.filter(header => +export class HeaderInterceptor { + public static createHeadersInterceptor(headers: Header[]): Interceptor { + const headersToAddOnce = headers.filter(header => header.onceOnlyHeaders.includes(header.name) ); - this.headersToAddEveryTime = headers.filter( + const headersToAddEveryTime = headers.filter( header => !header.onceOnlyHeaders.includes(header.name) ); - } - - public createHeadersInterceptor(): Interceptor { + let areOnlyOnceHeadersSent = false; return (options, nextCall) => { return new InterceptingCall(nextCall(options), { start: (metadata, listener, next) => { - this.headersToAddEveryTime.forEach(h => { + headersToAddEveryTime.forEach(h => { metadata.set(h.name, h.value); }); - if (!this.areOnlyOnceHeadersSent) { - this.areOnlyOnceHeadersSent = true; - this.headersToAddOnce.forEach(h => metadata.add(h.name, h.value)); + if (!areOnlyOnceHeadersSent) { + areOnlyOnceHeadersSent = true; + headersToAddOnce.forEach(h => metadata.add(h.name, h.value)); } next(metadata, {}); }, diff --git a/packages/client-sdk-nodejs/src/internal/grpc/retry-interceptor.ts b/packages/client-sdk-nodejs/src/internal/grpc/retry-interceptor.ts index 45db20b45..8c95586cc 100644 --- a/packages/client-sdk-nodejs/src/internal/grpc/retry-interceptor.ts +++ b/packages/client-sdk-nodejs/src/internal/grpc/retry-interceptor.ts @@ -14,37 +14,40 @@ import { } from '@grpc/grpc-js'; import {RetryStrategy} from '../../config/retry/retry-strategy'; import {Status} from '@grpc/grpc-js/build/src/constants'; -import {MomentoLoggerFactory, MomentoLogger} from '../../'; +import {MomentoLoggerFactory} from '../../'; +import {NoRetryStrategy} from '../../config/retry/no-retry-strategy'; -export function createRetryInterceptorIfEnabled( - loggerFactory: MomentoLoggerFactory, - retryStrategy: RetryStrategy -): Array { - return [ - new RetryInterceptor(loggerFactory, retryStrategy).createRetryInterceptor(), - ]; +export interface RetryInterceptorProps { + loggerFactory: MomentoLoggerFactory; + overallRequestTimeoutMs: number; + retryStrategy?: RetryStrategy; } export class RetryInterceptor { - private readonly logger: MomentoLogger; - private readonly retryStrategy: RetryStrategy; - - constructor( - loggerFactory: MomentoLoggerFactory, - retryStrategy: RetryStrategy - ) { - this.logger = loggerFactory.getLogger(this); - this.retryStrategy = retryStrategy; - } - // TODO: We need to send retry count information to the server so that we // will have some visibility into how often this is happening to customers: // https://github.com/momentohq/client-sdk-nodejs/issues/80 - public createRetryInterceptor(): Interceptor { - const logger = this.logger; - const retryStrategy = this.retryStrategy; + public static createRetryInterceptor( + props: RetryInterceptorProps + ): Interceptor { + const logger = props.loggerFactory.getLogger( + RetryInterceptor.constructor.name + ); + + const retryStrategy = + props.retryStrategy ?? + new NoRetryStrategy({loggerFactory: props.loggerFactory}); return (options, nextCall) => { + console.log('THE MAIN RETRY INTERCEPTOR FN IS CALLED'); + if (!options.deadline) { + const deadline = new Date(Date.now()); + deadline.setMilliseconds( + deadline.getMilliseconds() + props.overallRequestTimeoutMs + ); + options.deadline = deadline; + } + let savedMetadata: Metadata; let savedSendMessage: unknown; let savedReceiveMessage: unknown; @@ -67,7 +70,16 @@ export class RetryInterceptor { next: (arg0: any) => void ) { let attempts = 0; + const originalDeadline = options.deadline; + console.log( + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + `RETRY INTERCEPTOR: ORIGINAL DEADLINE: ${originalDeadline}` + ); const retry = function (message: unknown, metadata: Metadata) { + console.log( + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + `RECURSIVE RETRY; ORIGINAL DEADLINE: ${originalDeadline}, NEW DEADLINE: ${options.deadline}` + ); const newCall = nextCall(options); newCall.start(metadata, { onReceiveMessage: function (message) { diff --git a/packages/client-sdk-nodejs/src/internal/internal-auth-client.ts b/packages/client-sdk-nodejs/src/internal/internal-auth-client.ts index 8cf671390..056d8cc3f 100644 --- a/packages/client-sdk-nodejs/src/internal/internal-auth-client.ts +++ b/packages/client-sdk-nodejs/src/internal/internal-auth-client.ts @@ -1,7 +1,6 @@ import {auth, token} from '@gomomento/generated-types'; import grpcAuth = auth.auth; -import {Header, HeaderInterceptorProvider} from './grpc/headers-interceptor'; -import {ClientTimeoutInterceptor} from './grpc/client-timeout-interceptor'; +import {Header, HeaderInterceptor} from './grpc/headers-interceptor'; import {ChannelCredentials, Interceptor} from '@grpc/grpc-js'; import {version} from '../../package.json'; import {CacheServiceErrorMapper} from '../errors/cache-service-error-mapper'; @@ -58,6 +57,8 @@ import { isDisposableTokenCachePermission, isDisposableTokenPermissionsObject, } from '@gomomento/sdk-core/dist/src/auth/tokens/disposable-token-scope'; +import {RetryInterceptor} from './grpc/retry-interceptor'; +import {AuthClientConfigurations} from '../index'; export class InternalAuthClient implements IAuthClient { private static readonly REQUEST_TIMEOUT_MS: number = 60 * 1000; @@ -69,6 +70,8 @@ export class InternalAuthClient implements IAuthClient { private readonly authClient: grpcAuth.AuthClient; constructor(props: AuthClientProps) { + const configuration = + props.configuration ?? AuthClientConfigurations.Default.latest(); this.cacheServiceErrorMapper = new CacheServiceErrorMapper( props.throwOnErrors ?? false ); @@ -78,8 +81,11 @@ export class InternalAuthClient implements IAuthClient { new Header('runtime-version', `nodejs:${process.versions.node}`), ]; this.interceptors = [ - new HeaderInterceptorProvider(headers).createHeadersInterceptor(), - ClientTimeoutInterceptor(InternalAuthClient.REQUEST_TIMEOUT_MS), + HeaderInterceptor.createHeadersInterceptor(headers), + RetryInterceptor.createRetryInterceptor({ + loggerFactory: configuration.getLoggerFactory(), + overallRequestTimeoutMs: InternalAuthClient.REQUEST_TIMEOUT_MS, + }), ]; this.tokenClient = new token.token.TokenClient( this.creds.getTokenEndpoint(), diff --git a/packages/client-sdk-nodejs/src/internal/leaderboard-data-client.ts b/packages/client-sdk-nodejs/src/internal/leaderboard-data-client.ts index ea4b9ca3f..d51e59258 100644 --- a/packages/client-sdk-nodejs/src/internal/leaderboard-data-client.ts +++ b/packages/client-sdk-nodejs/src/internal/leaderboard-data-client.ts @@ -23,8 +23,7 @@ import {leaderboard} from '@gomomento/generated-types/dist/leaderboard'; import _Element = leaderboard._Element; import {IdleGrpcClientWrapper} from './grpc/idle-grpc-client-wrapper'; import {GrpcClientWrapper} from './grpc/grpc-client-wrapper'; -import {Header, HeaderInterceptorProvider} from './grpc/headers-interceptor'; -import {ClientTimeoutInterceptor} from './grpc/client-timeout-interceptor'; +import {Header, HeaderInterceptor} from './grpc/headers-interceptor'; import {CacheServiceErrorMapper} from '../errors/cache-service-error-mapper'; import { ChannelCredentials, @@ -42,6 +41,7 @@ import { } from '../config/middleware/middleware'; import {grpcChannelOptionsFromGrpcConfig} from './grpc/grpc-channel-options'; import {common} from '@gomomento/generated-types/dist/common'; +import {RetryInterceptor} from './grpc/retry-interceptor'; export const CONNECTION_ID_KEY = Symbol('connectionID'); @@ -142,8 +142,11 @@ export class LeaderboardDataClient implements ILeaderboardDataClient { middlewares, middlewareRequestContext ), - new HeaderInterceptorProvider(headers).createHeadersInterceptor(), - ClientTimeoutInterceptor(this.requestTimeoutMs), + HeaderInterceptor.createHeadersInterceptor(headers), + RetryInterceptor.createRetryInterceptor({ + loggerFactory: _loggerFactory, + overallRequestTimeoutMs: this.requestTimeoutMs, + }), ]; } diff --git a/packages/client-sdk-nodejs/src/internal/ping-client.ts b/packages/client-sdk-nodejs/src/internal/ping-client.ts index d6d5f3b58..1fec07f80 100644 --- a/packages/client-sdk-nodejs/src/internal/ping-client.ts +++ b/packages/client-sdk-nodejs/src/internal/ping-client.ts @@ -1,13 +1,13 @@ import {ping} from '@gomomento/generated-types'; import grpcPing = ping.cache_client; -import {Header, HeaderInterceptorProvider} from './grpc/headers-interceptor'; -import {ClientTimeoutInterceptor} from './grpc/client-timeout-interceptor'; +import {Header, HeaderInterceptor} from './grpc/headers-interceptor'; import {ChannelCredentials, Interceptor} from '@grpc/grpc-js'; import {version} from '../../package.json'; import {IdleGrpcClientWrapper} from './grpc/idle-grpc-client-wrapper'; import {GrpcClientWrapper} from './grpc/grpc-client-wrapper'; import {Configuration} from '../config/configuration'; import {CredentialProvider, MomentoLogger} from '../'; +import {RetryInterceptor} from './grpc/retry-interceptor'; export interface PingClientProps { configuration: Configuration; @@ -31,8 +31,11 @@ export class InternalNodeGrpcPingClient { new Header('runtime-version', `nodejs:${process.versions.node}`), ]; this.interceptors = [ - new HeaderInterceptorProvider(headers).createHeadersInterceptor(), - ClientTimeoutInterceptor(InternalNodeGrpcPingClient.REQUEST_TIMEOUT_MS), + HeaderInterceptor.createHeadersInterceptor(headers), + RetryInterceptor.createRetryInterceptor({ + loggerFactory: props.configuration.getLoggerFactory(), + overallRequestTimeoutMs: InternalNodeGrpcPingClient.REQUEST_TIMEOUT_MS, + }), ]; this.logger.debug( `Creating ping client using endpoint: '${props.endpoint}` diff --git a/packages/client-sdk-nodejs/src/internal/pubsub-client.ts b/packages/client-sdk-nodejs/src/internal/pubsub-client.ts index 373467d52..fa067ddc3 100644 --- a/packages/client-sdk-nodejs/src/internal/pubsub-client.ts +++ b/packages/client-sdk-nodejs/src/internal/pubsub-client.ts @@ -1,8 +1,7 @@ import {pubsub} from '@gomomento/generated-types'; import grpcPubsub = pubsub.cache_client.pubsub; // older versions of node don't have the global util variables https://github.com/nodejs/node/issues/20365 -import {Header, HeaderInterceptorProvider} from './grpc/headers-interceptor'; -import {ClientTimeoutInterceptor} from './grpc/client-timeout-interceptor'; +import {Header, HeaderInterceptor} from './grpc/headers-interceptor'; import {CacheServiceErrorMapper} from '../errors/cache-service-error-mapper'; import {ChannelCredentials, Interceptor, ServiceError} from '@grpc/grpc-js'; import {version} from '../../package.json'; @@ -25,6 +24,7 @@ import { import {TopicConfiguration} from '../config/topic-configuration'; import {TopicClientPropsWithConfiguration} from './topic-client-props-with-config'; import {grpcChannelOptionsFromGrpcConfig} from './grpc/grpc-channel-options'; +import {RetryInterceptor} from './grpc/retry-interceptor'; export class PubsubClient extends AbstractPubsubClient { private readonly client: grpcPubsub.PubsubClient; @@ -299,8 +299,11 @@ export class PubsubClient extends AbstractPubsubClient { ): Interceptor[] { return [ middlewaresInterceptor(configuration.getLoggerFactory(), [], {}), - new HeaderInterceptorProvider(headers).createHeadersInterceptor(), - ClientTimeoutInterceptor(requestTimeoutMs), + HeaderInterceptor.createHeadersInterceptor(headers), + RetryInterceptor.createRetryInterceptor({ + loggerFactory: configuration.getLoggerFactory(), + overallRequestTimeoutMs: requestTimeoutMs, + }), ]; } @@ -309,6 +312,6 @@ export class PubsubClient extends AbstractPubsubClient { private static initializeStreamingInterceptors( headers: Header[] ): Interceptor[] { - return [new HeaderInterceptorProvider(headers).createHeadersInterceptor()]; + return [HeaderInterceptor.createHeadersInterceptor(headers)]; } } diff --git a/packages/client-sdk-nodejs/src/internal/storage-control-client.ts b/packages/client-sdk-nodejs/src/internal/storage-control-client.ts index 1e941594d..332535846 100644 --- a/packages/client-sdk-nodejs/src/internal/storage-control-client.ts +++ b/packages/client-sdk-nodejs/src/internal/storage-control-client.ts @@ -1,7 +1,6 @@ import {control} from '@gomomento/generated-types'; import grpcControl = control.control_client; -import {Header, HeaderInterceptorProvider} from './grpc/headers-interceptor'; -import {ClientTimeoutInterceptor} from './grpc/client-timeout-interceptor'; +import {Header, HeaderInterceptor} from './grpc/headers-interceptor'; import {CacheServiceErrorMapper} from '../errors/cache-service-error-mapper'; import {ChannelCredentials, Interceptor} from '@grpc/grpc-js'; import {MomentoLogger, StoreInfo, ListStores, MomentoErrorCode} from '..'; @@ -9,6 +8,7 @@ import {version} from '../../package.json'; import {validateStoreName} from '@gomomento/sdk-core/dist/src/internal/utils'; import {CreateStore, DeleteStore} from '@gomomento/sdk-core'; import {StorageClientPropsWithConfig} from './storage-client-props-with-config'; +import {RetryInterceptor} from './grpc/retry-interceptor'; export class StorageControlClient { private readonly clientWrapper: grpcControl.ScsControlClient; @@ -29,8 +29,11 @@ export class StorageControlClient { new Header('runtime-version', `nodejs:${process.versions.node}`), ]; this.interceptors = [ - new HeaderInterceptorProvider(headers).createHeadersInterceptor(), - ClientTimeoutInterceptor(StorageControlClient.REQUEST_TIMEOUT_MS), + HeaderInterceptor.createHeadersInterceptor(headers), + RetryInterceptor.createRetryInterceptor({ + loggerFactory: props.configuration.getLoggerFactory(), + overallRequestTimeoutMs: StorageControlClient.REQUEST_TIMEOUT_MS, + }), ]; this.logger.debug( `Creating storage control client using endpoint: '${props.credentialProvider.getControlEndpoint()}` diff --git a/packages/client-sdk-nodejs/src/internal/storage-data-client.ts b/packages/client-sdk-nodejs/src/internal/storage-data-client.ts index 0d95a502b..115736ef1 100644 --- a/packages/client-sdk-nodejs/src/internal/storage-data-client.ts +++ b/packages/client-sdk-nodejs/src/internal/storage-data-client.ts @@ -11,7 +11,7 @@ import { } from '@gomomento/sdk-core'; import {validateStoreName} from '@gomomento/sdk-core/dist/src/internal/utils'; import {store} from '@gomomento/generated-types/dist/store'; -import {Header, HeaderInterceptorProvider} from './grpc/headers-interceptor'; +import {Header, HeaderInterceptor} from './grpc/headers-interceptor'; import { ChannelCredentials, Interceptor, @@ -25,8 +25,7 @@ import {StorageConfiguration} from '../config/storage-configuration'; import {StorageClientPropsWithConfig} from './storage-client-props-with-config'; import {StaticGrpcConfiguration} from '../config/transport/cache'; import {CacheServiceErrorMapper} from '../errors/cache-service-error-mapper'; -import {createRetryInterceptorIfEnabled} from './grpc/retry-interceptor'; -import {ClientTimeoutInterceptor} from './grpc/client-timeout-interceptor'; +import {RetryInterceptor} from './grpc/retry-interceptor'; export class StorageDataClient implements IStorageDataClient { private readonly configuration: StorageConfiguration; @@ -114,17 +113,12 @@ export class StorageDataClient implements IStorageDataClient { ]; return [ - ...createRetryInterceptorIfEnabled( - this.configuration.getLoggerFactory(), - this.configuration.getRetryStrategy() - ), - new HeaderInterceptorProvider(headers).createHeadersInterceptor(), - // For the timeout interceptors to work correctly, it must be specified last. - ClientTimeoutInterceptor( - this.requestTimeoutMs, - this.configuration.getRetryStrategy(), - _loggerFactory - ), + HeaderInterceptor.createHeadersInterceptor(headers), + RetryInterceptor.createRetryInterceptor({ + loggerFactory: this.configuration.getLoggerFactory(), + retryStrategy: this.configuration.getRetryStrategy(), + overallRequestTimeoutMs: this.requestTimeoutMs, + }), ]; } diff --git a/packages/client-sdk-nodejs/src/internal/webhook-client.ts b/packages/client-sdk-nodejs/src/internal/webhook-client.ts index 32e02a076..402774d3c 100644 --- a/packages/client-sdk-nodejs/src/internal/webhook-client.ts +++ b/packages/client-sdk-nodejs/src/internal/webhook-client.ts @@ -14,9 +14,8 @@ import { } from '@gomomento/sdk-core'; import {ChannelCredentials, Interceptor} from '@grpc/grpc-js'; import {IWebhookClient} from '@gomomento/sdk-core/dist/src/internal/clients/pubsub/IWebhookClient'; -import {Header, HeaderInterceptorProvider} from './grpc/headers-interceptor'; +import {Header, HeaderInterceptor} from './grpc/headers-interceptor'; import {version} from '../../package.json'; -import {ClientTimeoutInterceptor} from './grpc/client-timeout-interceptor'; import {CacheServiceErrorMapper} from '../errors/cache-service-error-mapper'; import { validateCacheName, @@ -24,6 +23,7 @@ import { validateWebhookName, } from '@gomomento/sdk-core/dist/src/internal/utils'; import {TopicClientPropsWithConfiguration} from './topic-client-props-with-config'; +import {RetryInterceptor} from './grpc/retry-interceptor'; export class WebhookClient implements IWebhookClient { private readonly webhookClient: grpcWebhook.WebhookClient; @@ -48,8 +48,11 @@ export class WebhookClient implements IWebhookClient { new Header('runtime-version', `nodejs:${process.versions.node}`), ]; this.unaryInterceptors = [ - new HeaderInterceptorProvider(headers).createHeadersInterceptor(), - ClientTimeoutInterceptor(WebhookClient.DEFAULT_REQUEST_TIMEOUT_MS), + HeaderInterceptor.createHeadersInterceptor(headers), + RetryInterceptor.createRetryInterceptor({ + loggerFactory: props.configuration.getLoggerFactory(), + overallRequestTimeoutMs: WebhookClient.DEFAULT_REQUEST_TIMEOUT_MS, + }), ]; this.webhookClient = new webhook.webhook.WebhookClient( props.credentialProvider.getControlEndpoint(), From 017d87ce04e4e78627f84bbb52c1da4533c7af24 Mon Sep 17 00:00:00 2001 From: Chris Price Date: Mon, 26 Aug 2024 13:04:42 -0700 Subject: [PATCH 40/43] fix: accidentally checked in local ref for storage examples --- examples/nodejs/storage/package-lock.json | 883 +++++++++++++++++++--- examples/nodejs/storage/package.json | 2 +- 2 files changed, 787 insertions(+), 98 deletions(-) diff --git a/examples/nodejs/storage/package-lock.json b/examples/nodejs/storage/package-lock.json index 787c8bc06..008b5d25a 100644 --- a/examples/nodejs/storage/package-lock.json +++ b/examples/nodejs/storage/package-lock.json @@ -9,7 +9,7 @@ "version": "1.0.0", "license": "ISC", "dependencies": { - "@gomomento/sdk": "file:../../../packages/client-sdk-nodejs" + "@gomomento/sdk": "^1.95.0" }, "devDependencies": { "@types/node": "16.11.4", @@ -27,43 +27,6 @@ "node": ">=10.4.0" } }, - "../../../packages/client-sdk-nodejs": { - "version": "0.0.1", - "license": "Apache-2.0", - "dependencies": { - "@gomomento/generated-types": "0.113.0", - "@gomomento/sdk-core": "file:../core", - "@grpc/grpc-js": "1.10.9", - "@types/google-protobuf": "3.15.10", - "google-protobuf": "3.21.2", - "jwt-decode": "3.1.2" - }, - "devDependencies": { - "@gomomento/common-integration-tests": "file:../common-integration-tests", - "@types/jest": "27.5.2", - "@types/node": "16.18.97", - "@types/uuid": "8.3.4", - "@typescript-eslint/eslint-plugin": "5.62.0", - "@typescript-eslint/parser": "5.62.0", - "eslint": "7.32.0", - "eslint-config-prettier": "8.10.0", - "eslint-plugin-import": "2.29.0", - "eslint-plugin-node": "11.1.0", - "eslint-plugin-prettier": "4.2.1", - "jest": "29.7.0", - "jest-extended": "4.0.2", - "jest-spec-reporter": "^1.0.19", - "prettier": "2.8.8", - "ts-jest": "29.1.1", - "ts-morph": "22.0.0", - "ts-node": "10.9.1", - "typescript": "4.9.5", - "uuid": "8.3.2" - }, - "engines": { - "node": ">= 16" - } - }, "node_modules/@eslint/eslintrc": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.4.1.tgz", @@ -87,14 +50,79 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/@gomomento/generated-types": { + "version": "0.113.0", + "resolved": "https://registry.npmjs.org/@gomomento/generated-types/-/generated-types-0.113.0.tgz", + "integrity": "sha512-7DJdcNWzCT5dpp1W+Vb77RZfamCt/SbtvoOcDtSDVU/wAsK7y7Yeo88DkgqiD2sCHv/u5X3wgLye51dlCsbgDw==", + "dependencies": { + "@grpc/grpc-js": "1.10.9", + "google-protobuf": "3.21.2", + "grpc-tools": "^1.12.4", + "protoc-gen-ts": "^0.8.6" + } + }, "node_modules/@gomomento/sdk": { - "resolved": "../../../packages/client-sdk-nodejs", - "link": true + "version": "1.96.0", + "resolved": "https://registry.npmjs.org/@gomomento/sdk/-/sdk-1.96.0.tgz", + "integrity": "sha512-CErzGZjCuiXaw+GjHRjFqkR8lmrnquyB/LFBEsunSpfafiI7D6J8NhFrFGbgy/b6dmjxOC2UYRVEtY1NP8rJ9w==", + "dependencies": { + "@gomomento/generated-types": "0.113.0", + "@gomomento/sdk-core": "1.96.0", + "@grpc/grpc-js": "1.10.9", + "@types/google-protobuf": "3.15.10", + "google-protobuf": "3.21.2", + "jwt-decode": "3.1.2" + }, + "engines": { + "node": ">= 16" + } + }, + "node_modules/@gomomento/sdk-core": { + "version": "1.96.0", + "resolved": "https://registry.npmjs.org/@gomomento/sdk-core/-/sdk-core-1.96.0.tgz", + "integrity": "sha512-2MtULKUgDTwYqUuOVL+6+Z1eclruMfJfi/21MGPMTBwj+52A4QKLocXisssoOD35AFQ23GmJ+QMYRFwk78DK1A==", + "dependencies": { + "buffer": "6.0.3", + "jwt-decode": "3.1.2" + }, + "engines": { + "node": ">= 16" + } + }, + "node_modules/@grpc/grpc-js": { + "version": "1.10.9", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.10.9.tgz", + "integrity": "sha512-5tcgUctCG0qoNyfChZifz2tJqbRbXVO9J7X6duFcOjY3HUNCxg5D0ZCK7EP9vIcZ0zRpLU9bWkyCqVCLZ46IbQ==", + "dependencies": { + "@grpc/proto-loader": "^0.7.13", + "@js-sdsl/ordered-map": "^4.4.2" + }, + "engines": { + "node": ">=12.10.0" + } + }, + "node_modules/@grpc/proto-loader": { + "version": "0.7.13", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.13.tgz", + "integrity": "sha512-AiXO/bfe9bmxBjxxtYxFAXGZvMaN5s8kO+jBHAJCON8rJoB5YS/D6X7ZNc6XQkuHNmyl4CYaMI1fJ/Gn27RGGw==", + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.2.5", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } }, "node_modules/@humanwhocodes/config-array": { "version": "0.9.5", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.9.5.tgz", "integrity": "sha512-ObyMyWxZiCu/yTisA7uzx81s40xR2fD5Cg/2Kq7G02ajkNubJf6BopgDTmDyc3U7sXpNKM8cYOw7s7Tyr+DnCw==", + "deprecated": "Use @eslint/config-array instead", "dev": true, "dependencies": { "@humanwhocodes/object-schema": "^1.2.1", @@ -109,8 +137,37 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", + "deprecated": "Use @eslint/object-schema instead", "dev": true }, + "node_modules/@js-sdsl/ordered-map": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz", + "integrity": "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/js-sdsl" + } + }, + "node_modules/@mapbox/node-pre-gyp": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz", + "integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==", + "dependencies": { + "detect-libc": "^2.0.0", + "https-proxy-agent": "^5.0.0", + "make-dir": "^3.1.0", + "node-fetch": "^2.6.7", + "nopt": "^5.0.0", + "npmlog": "^5.0.1", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.11" + }, + "bin": { + "node-pre-gyp": "bin/node-pre-gyp" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -146,6 +203,65 @@ "node": ">= 8" } }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==" + }, + "node_modules/@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==" + }, + "node_modules/@types/google-protobuf": { + "version": "3.15.10", + "resolved": "https://registry.npmjs.org/@types/google-protobuf/-/google-protobuf-3.15.10.tgz", + "integrity": "sha512-uiyKJCa8hbmPE4yxwjbkMOALaBAiOVcatW/yEGbjTqwAh4kzNgQPWRlJMNPXpB5CPUM66xsYufiSX9WKHZCE9g==" + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -161,8 +277,7 @@ "node_modules/@types/node": { "version": "16.11.4", "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.4.tgz", - "integrity": "sha512-TMgXmy0v2xWyuCSCJM6NCna2snndD8yvQF67J29ipdzMcsPa9u+o0tjF5+EQNdhcuZplYuouYqpc4zcd5I6amQ==", - "dev": true + "integrity": "sha512-TMgXmy0v2xWyuCSCJM6NCna2snndD8yvQF67J29ipdzMcsPa9u+o0tjF5+EQNdhcuZplYuouYqpc4zcd5I6amQ==" }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "5.30.5", @@ -465,10 +580,15 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" + }, "node_modules/acorn": { - "version": "8.11.3", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", - "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", + "version": "8.12.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", + "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", "dev": true, "bin": { "acorn": "bin/acorn" @@ -486,6 +606,17 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -506,7 +637,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, "engines": { "node": ">=8" } @@ -515,7 +645,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -526,6 +655,24 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/aproba": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", + "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==" + }, + "node_modules/are-we-there-yet": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", + "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", + "deprecated": "This package is no longer supported.", + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -635,14 +782,31 @@ "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -660,6 +824,29 @@ "node": ">=8" } }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, "node_modules/call-bind": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", @@ -704,11 +891,31 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "engines": { + "node": ">=10" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "dependencies": { "color-name": "~1.1.4" }, @@ -719,14 +926,25 @@ "node_modules/color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "bin": { + "color-support": "bin.js" + } }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" + }, + "node_modules/console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==" }, "node_modules/cross-spawn": { "version": "7.0.3", @@ -794,10 +1012,9 @@ } }, "node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", + "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", "dependencies": { "ms": "2.1.2" }, @@ -850,6 +1067,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==" + }, + "node_modules/detect-libc": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", + "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", + "engines": { + "node": ">=8" + } + }, "node_modules/dir-glob": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", @@ -874,6 +1104,11 @@ "node": ">=6.0.0" } }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, "node_modules/es-abstract": { "version": "1.23.3", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.3.tgz", @@ -1007,6 +1242,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/escalade": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", + "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", + "engines": { + "node": ">=6" + } + }, "node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -1104,9 +1347,9 @@ } }, "node_modules/eslint-module-utils": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.8.1.tgz", - "integrity": "sha512-rXDXR3h7cs7dy9RNpUlQf80nX31XWJEyGq1tRMo+6GsO5VmTe4UTwtmonAD4ZkAsrfMVDA2wlGJ3790Ys+D49Q==", + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.8.2.tgz", + "integrity": "sha512-3XnC5fDyc8M4J2E8pt8pmSVRX2M+5yWMCfI/kDZwauQeFgzQOuhcRBFKjTeJagqgk4sFKxe1mvNVnaWwImx/Tg==", "dev": true, "dependencies": { "debug": "^3.2.7" @@ -1395,9 +1638,9 @@ } }, "node_modules/esquery": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", - "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", "dev": true, "dependencies": { "estraverse": "^5.1.0" @@ -1568,11 +1811,32 @@ "is-callable": "^1.1.3" } }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" }, "node_modules/function-bind": { "version": "1.1.2", @@ -1616,6 +1880,34 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/gauge": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", + "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", + "deprecated": "This package is no longer supported.", + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.2", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.1", + "object-assign": "^4.1.1", + "signal-exit": "^3.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, "node_modules/get-intrinsic": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", @@ -1657,7 +1949,6 @@ "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", "deprecated": "Glob versions prior to v9 are no longer supported", - "dev": true, "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -1736,6 +2027,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/google-protobuf": { + "version": "3.21.2", + "resolved": "https://registry.npmjs.org/google-protobuf/-/google-protobuf-3.21.2.tgz", + "integrity": "sha512-3MSOYFO5U9mPGikIYCzK0SaThypfGgS6bHqrUGXG3DPHCrb+txNqeEcns1W0lkGfk0rCyNXm7xB9rMxnCiZOoA==" + }, "node_modules/gopd": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", @@ -1748,6 +2044,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/grpc-tools": { + "version": "1.12.4", + "resolved": "https://registry.npmjs.org/grpc-tools/-/grpc-tools-1.12.4.tgz", + "integrity": "sha512-5+mLAJJma3BjnW/KQp6JBjUMgvu7Mu3dBvBPd1dcbNIb+qiR0817zDpgPjS7gRb+l/8EVNIa3cB02xI9JLToKg==", + "hasInstallScript": true, + "dependencies": { + "@mapbox/node-pre-gyp": "^1.0.5" + }, + "bin": { + "grpc_tools_node_protoc": "bin/protoc.js", + "grpc_tools_node_protoc_plugin": "bin/protoc_plugin.js" + } + }, "node_modules/has": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/has/-/has-1.0.4.tgz", @@ -1826,6 +2135,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==" + }, "node_modules/hasown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", @@ -1838,10 +2152,41 @@ "node": ">= 0.4" } }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, "node_modules/ignore": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", - "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", "dev": true, "engines": { "node": ">= 4" @@ -1877,7 +2222,6 @@ "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", - "dev": true, "dependencies": { "once": "^1.3.0", "wrappy": "1" @@ -1886,8 +2230,7 @@ "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, "node_modules/internal-slot": { "version": "1.0.7", @@ -1960,12 +2303,15 @@ } }, "node_modules/is-core-module": { - "version": "2.13.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", - "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz", + "integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==", "dev": true, "dependencies": { - "hasown": "^2.0.0" + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -2010,6 +2356,14 @@ "node": ">=0.10.0" } }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "engines": { + "node": ">=8" + } + }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -2200,6 +2554,11 @@ "json5": "lib/cli.js" } }, + "node_modules/jwt-decode": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-3.1.2.tgz", + "integrity": "sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A==" + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -2222,12 +2581,44 @@ "node": ">= 0.8.0" } }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, + "node_modules/long": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz", + "integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==" + }, + "node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -2238,9 +2629,9 @@ } }, "node_modules/micromatch": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz", - "integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==", + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dev": true, "dependencies": { "braces": "^3.0.3", @@ -2254,7 +2645,6 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, "dependencies": { "brace-expansion": "^1.1.7" }, @@ -2271,11 +2661,52 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "node_modules/natural-compare": { "version": "1.4.0", @@ -2283,11 +2714,67 @@ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/npmlog": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", + "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", + "deprecated": "This package is no longer supported.", + "dependencies": { + "are-we-there-yet": "^2.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^3.0.0", + "set-blocking": "^2.0.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/object-inspect": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", - "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", + "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==", "dev": true, + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -2340,7 +2827,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, "dependencies": { "wrappy": "1" } @@ -2378,7 +2864,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -2464,6 +2949,41 @@ "node": ">=6.0.0" } }, + "node_modules/protobufjs": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.4.0.tgz", + "integrity": "sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw==", + "hasInstallScript": true, + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/protoc-gen-ts": { + "version": "0.8.7", + "resolved": "https://registry.npmjs.org/protoc-gen-ts/-/protoc-gen-ts-0.8.7.tgz", + "integrity": "sha512-jr4VJey2J9LVYCV7EVyVe53g1VMw28cCmYJhBe5e3YX5wiyiDwgxWxeDf9oTqAe4P1bN/YGAkW2jhlH8LohwiQ==", + "bin": { + "protoc-gen-ts": "protoc-gen-ts.js" + }, + "funding": { + "type": "individual", + "url": "https://www.buymeacoffee.com/thesayyn" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -2493,6 +3013,19 @@ } ] }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/regexp.prototype.flags": { "version": "1.5.2", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz", @@ -2523,6 +3056,14 @@ "url": "https://github.com/sponsors/mysticatea" } }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/resolve": { "version": "1.22.8", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", @@ -2564,7 +3105,6 @@ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", "deprecated": "Rimraf versions prior to v4 are no longer supported", - "dev": true, "dependencies": { "glob": "^7.1.3" }, @@ -2616,6 +3156,25 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, "node_modules/safe-regex-test": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.3.tgz", @@ -2634,10 +3193,9 @@ } }, "node_modules/semver": { - "version": "7.6.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", - "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", - "dev": true, + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", "bin": { "semver": "bin/semver.js" }, @@ -2645,6 +3203,11 @@ "node": ">=10" } }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==" + }, "node_modules/set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", @@ -2716,6 +3279,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" + }, "node_modules/slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", @@ -2725,6 +3293,27 @@ "node": ">=8" } }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/string.prototype.trim": { "version": "1.2.9", "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.9.tgz", @@ -2778,7 +3367,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, "dependencies": { "ansi-regex": "^5.0.1" }, @@ -2831,6 +3419,22 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -2849,6 +3453,11 @@ "node": ">=8.0" } }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, "node_modules/tsconfig-paths": { "version": "3.15.0", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", @@ -3016,12 +3625,31 @@ "punycode": "^2.1.0" } }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, "node_modules/v8-compile-cache": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.4.0.tgz", "integrity": "sha512-ocyWc3bAHBB/guyqJQVI5o4BZkPhznPYUG2ea80Gond/BgNWpap8TOmLSeeQG7bnh2KMISxskdADG59j7zruhw==", "dev": true }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -3072,6 +3700,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "dependencies": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", @@ -3081,11 +3717,64 @@ "node": ">=0.10.0" } }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "engines": { + "node": ">=12" + } } } } diff --git a/examples/nodejs/storage/package.json b/examples/nodejs/storage/package.json index 927a2a40c..e808d80e7 100644 --- a/examples/nodejs/storage/package.json +++ b/examples/nodejs/storage/package.json @@ -28,7 +28,7 @@ "typescript": "4.9.5" }, "dependencies": { - "@gomomento/sdk": "file:../../../packages/client-sdk-nodejs" + "@gomomento/sdk": "^1.95.0" }, "engines": { "node": ">=10.4.0" From f80becc0ce03de04cedc711450486687052b0456 Mon Sep 17 00:00:00 2001 From: Chris Price Date: Mon, 26 Aug 2024 13:10:23 -0700 Subject: [PATCH 41/43] fix: remove more local changes to basic storage example --- examples/nodejs/storage/basic.ts | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/examples/nodejs/storage/basic.ts b/examples/nodejs/storage/basic.ts index d4f7352d9..005ecfe27 100644 --- a/examples/nodejs/storage/basic.ts +++ b/examples/nodejs/storage/basic.ts @@ -1,26 +1,15 @@ import { CreateStoreResponse, CredentialProvider, - DefaultMomentoLoggerFactory, - DefaultMomentoLoggerLevel, PreviewStorageClient, StorageConfigurations, StorageGetResponse, StoragePutResponse, } from '@gomomento/sdk'; -import {FixedTimeoutRetryStrategy} from '@gomomento/sdk/dist/src/config/retry/fixed-timeout-retry-strategy'; async function main() { - const loggerFactory = new DefaultMomentoLoggerFactory(DefaultMomentoLoggerLevel.INFO); - - const retryStrategy = new FixedTimeoutRetryStrategy({ - loggerFactory: loggerFactory, - responseDataReceivedTimeoutMillis: 100, - retryDelayIntervalMillis: 1000, - }); - const storageClient = new PreviewStorageClient({ - configuration: StorageConfigurations.Laptop.latest(loggerFactory).withRetryStrategy(retryStrategy), + configuration: StorageConfigurations.Laptop.latest(), credentialProvider: CredentialProvider.fromEnvironmentVariable('MOMENTO_API_KEY').withMomentoLocal(), }); From ac12515f7252d8dd0e7568b63e53d02320c900b3 Mon Sep 17 00:00:00 2001 From: Chris Price Date: Mon, 26 Aug 2024 14:38:01 -0700 Subject: [PATCH 42/43] chore: finish migrating client timeout logic into retry interceptor --- .../retry/fixed-timeout-retry-strategy.ts | 2 +- .../src/internal/cache-control-client.ts | 1 + .../src/internal/cache-data-client.ts | 1 + .../grpc/client-timeout-interceptor.ts | 162 ------------------ .../src/internal/grpc/retry-interceptor.ts | 79 ++++++--- .../src/internal/internal-auth-client.ts | 1 + .../src/internal/leaderboard-data-client.ts | 1 + .../src/internal/ping-client.ts | 1 + .../src/internal/pubsub-client.ts | 1 + .../src/internal/storage-control-client.ts | 1 + .../src/internal/storage-data-client.ts | 4 +- .../src/internal/webhook-client.ts | 1 + 12 files changed, 70 insertions(+), 185 deletions(-) delete mode 100644 packages/client-sdk-nodejs/src/internal/grpc/client-timeout-interceptor.ts diff --git a/packages/client-sdk-nodejs/src/config/retry/fixed-timeout-retry-strategy.ts b/packages/client-sdk-nodejs/src/config/retry/fixed-timeout-retry-strategy.ts index c9c4a4aa4..8697dcdc9 100644 --- a/packages/client-sdk-nodejs/src/config/retry/fixed-timeout-retry-strategy.ts +++ b/packages/client-sdk-nodejs/src/config/retry/fixed-timeout-retry-strategy.ts @@ -45,7 +45,7 @@ export class FixedTimeoutRetryStrategy implements RetryStrategy { } this.logger.debug( - `Request is eligible for retry (attempt ${props.attemptNumber}), retrying after ${this.retryDelayIntervalMillis} +/- some jitter.` + `Request is eligible for retry (attempt ${props.attemptNumber}), retrying after ${this.retryDelayIntervalMillis} ms +/- jitter.` ); // retry after a fixed time interval has passed (+/- some jitter) return addJitter(this.retryDelayIntervalMillis); diff --git a/packages/client-sdk-nodejs/src/internal/cache-control-client.ts b/packages/client-sdk-nodejs/src/internal/cache-control-client.ts index 8cfc21ee7..c8f2653bf 100644 --- a/packages/client-sdk-nodejs/src/internal/cache-control-client.ts +++ b/packages/client-sdk-nodejs/src/internal/cache-control-client.ts @@ -52,6 +52,7 @@ export class CacheControlClient { this.interceptors = [ HeaderInterceptor.createHeadersInterceptor(headers), RetryInterceptor.createRetryInterceptor({ + clientName: 'CacheControlClient', loggerFactory: props.configuration.getLoggerFactory(), overallRequestTimeoutMs: CacheControlClient.REQUEST_TIMEOUT_MS, }), diff --git a/packages/client-sdk-nodejs/src/internal/cache-data-client.ts b/packages/client-sdk-nodejs/src/internal/cache-data-client.ts index 6bed3360f..1f6db3f08 100644 --- a/packages/client-sdk-nodejs/src/internal/cache-data-client.ts +++ b/packages/client-sdk-nodejs/src/internal/cache-data-client.ts @@ -4121,6 +4121,7 @@ export class CacheDataClient implements IDataClient { ), HeaderInterceptor.createHeadersInterceptor(headers), RetryInterceptor.createRetryInterceptor({ + clientName: 'CacheDataClient', loggerFactory: this.configuration.getLoggerFactory(), retryStrategy: this.configuration.getRetryStrategy(), overallRequestTimeoutMs: this.requestTimeoutMs, diff --git a/packages/client-sdk-nodejs/src/internal/grpc/client-timeout-interceptor.ts b/packages/client-sdk-nodejs/src/internal/grpc/client-timeout-interceptor.ts deleted file mode 100644 index bbb2b2caf..000000000 --- a/packages/client-sdk-nodejs/src/internal/grpc/client-timeout-interceptor.ts +++ /dev/null @@ -1,162 +0,0 @@ -// import {MomentoLogger, MomentoLoggerFactory} from '@gomomento/sdk-core'; -// import {InterceptingCall, Interceptor, status} from '@grpc/grpc-js'; -// import {RetryStrategy} from '../../config/retry/retry-strategy'; -// import {DefaultMomentoLoggerFactory} from '../../config/logging/default-momento-logger'; -// -// // Determine which retry strategy is specified in the configuration -// // and which interceptor to use. -// export const ClientTimeoutInterceptor = ( -// overallRequestTimeoutMs: number, -// retryStrategy?: RetryStrategy, -// loggerFactory?: MomentoLoggerFactory -// ): Interceptor => { -// if ( -// retryStrategy !== undefined && -// retryStrategy.responseDataReceivedTimeoutMillis !== undefined -// ) { -// return new RetryUntilTimeoutInterceptor( -// loggerFactory ?? new DefaultMomentoLoggerFactory(), -// overallRequestTimeoutMs, -// retryStrategy.responseDataReceivedTimeoutMillis -// ).createTimeoutInterceptor(); -// } -// return new BasicTimeoutInterceptor( -// overallRequestTimeoutMs -// ).createTimeoutInterceptor(); -// }; -// -// class BasicTimeoutInterceptor { -// private readonly overallRequestTimeoutMs: number; -// -// constructor(overallRequestTimeoutMs: number) { -// this.overallRequestTimeoutMs = overallRequestTimeoutMs; -// } -// -// public createTimeoutInterceptor(): Interceptor { -// return (options, nextCall) => { -// if (!options.deadline) { -// const deadline = new Date(Date.now()); -// deadline.setMilliseconds( -// deadline.getMilliseconds() + this.overallRequestTimeoutMs -// ); -// options.deadline = deadline; -// } -// return new InterceptingCall(nextCall(options)); -// }; -// } -// } -// -// class RetryUntilTimeoutInterceptor { -// private readonly logger: MomentoLogger; -// private readonly responseDataReceivedTimeoutMs: number; -// private readonly overallRequestTimeoutMs: number; -// private overallDeadline?: Date; -// private previousRpc?: string; -// -// constructor( -// loggerFactory: MomentoLoggerFactory, -// overallRequestTimeoutMs: number, -// responseDataReceivedTimeoutMs: number -// ) { -// this.logger = loggerFactory.getLogger(this); -// this.responseDataReceivedTimeoutMs = responseDataReceivedTimeoutMs; -// this.overallRequestTimeoutMs = overallRequestTimeoutMs; -// } -// -// public createTimeoutInterceptor(): Interceptor { -// return (options, nextCall) => { -// this.logger.debug( -// `Previous RPC: ${this.previousRpc ?? 'none'} | Incoming RPC: ${ -// options.method_definition.path -// }` -// ); -// -// console.log('The client timeout interceptor has been called'); -// -// // If the received deadline is equal to the overall deadline, we've -// // maxed out the retries on a particular request and should cancel retries. -// const receivedDeadline = options.deadline; -// if ( -// receivedDeadline !== undefined && -// receivedDeadline === this.overallDeadline -// ) { -// this.logger.debug( -// `Unable to successfully retry request within overall timeout of ${this.overallRequestTimeoutMs}ms, canceling request` -// ); -// // reset overall deadline for next request -// this.overallDeadline = undefined; -// this.previousRpc = undefined; -// const call = new InterceptingCall(nextCall(options)); -// console.log( -// 'The client timeout interceptor is returning; detected timeout' -// ); -// call.cancelWithStatus( -// status.CANCELLED, -// `Unable to successfully retry request within overall timeout of ${this.overallRequestTimeoutMs}ms` -// ); -// return call; -// } -// -// // Reset overall and incremental deadlines in these cases: -// // Case 1: overallDeadline is undefined (first request or previous was canceled) -// // Case 2: different RPC path requested through same client -// // Case 3: new request through same client after original retry deadline has passed -// // (Note: Case 3 check must occur after the receivedDeadline == options.deadline check -// // or else the request is always retried). -// if ( -// this.overallDeadline === undefined || -// this.previousRpc !== options.method_definition.path || -// (this.overallDeadline !== undefined && -// Date.now().valueOf() > this.overallDeadline.valueOf()) -// ) { -// this.previousRpc = options.method_definition.path; -// this.overallDeadline = createNewDeadline(this.overallRequestTimeoutMs); -// options.deadline = createNewDeadline( -// this.responseDataReceivedTimeoutMs -// ); -// this.logger.debug( -// `Overall deadline set to ${this.overallDeadline.valueOf()}, which is ${ -// this.overallRequestTimeoutMs -// }ms from now` -// ); -// this.logger.debug( -// `Incremental deadline set to ${options.deadline.valueOf()}, which is ${ -// this.responseDataReceivedTimeoutMs -// }ms from now` -// ); -// console.log( -// 'The client timeout interceptor is returning; reset overall and incremental deadline' -// ); -// return new InterceptingCall(nextCall(options)); -// } -// -// // Otherwise, we've hit an incremental timeout and must set the next deadline. -// const newDeadline = createNewDeadline(this.responseDataReceivedTimeoutMs); -// if (newDeadline > this.overallDeadline) { -// this.logger.debug( -// `New incremental deadline ${newDeadline.valueOf()} would exceed overall deadline, setting to overall deadline ${this.overallDeadline.valueOf()}` -// ); -// options.deadline = this.overallDeadline; -// } else { -// this.logger.debug( -// `Incremental deadline set to ${newDeadline.valueOf()}, which is ${ -// this.responseDataReceivedTimeoutMs -// }ms from now` -// ); -// options.deadline = newDeadline; -// } -// this.previousRpc = options.method_definition.path; -// -// console.log( -// 'The client timeout interceptor is returning; detected incremental timeout' -// ); -// return new InterceptingCall(nextCall(options)); -// }; -// } -// } -// -// function createNewDeadline(timeToAddMillis: number): Date { -// const deadline = new Date(Date.now()); -// deadline.setMilliseconds(deadline.getMilliseconds() + timeToAddMillis); -// return deadline; -// } diff --git a/packages/client-sdk-nodejs/src/internal/grpc/retry-interceptor.ts b/packages/client-sdk-nodejs/src/internal/grpc/retry-interceptor.ts index 8c95586cc..46cc35fd3 100644 --- a/packages/client-sdk-nodejs/src/internal/grpc/retry-interceptor.ts +++ b/packages/client-sdk-nodejs/src/internal/grpc/retry-interceptor.ts @@ -18,6 +18,7 @@ import {MomentoLoggerFactory} from '../../'; import {NoRetryStrategy} from '../../config/retry/no-retry-strategy'; export interface RetryInterceptorProps { + clientName: string; loggerFactory: MomentoLoggerFactory; overallRequestTimeoutMs: number; retryStrategy?: RetryStrategy; @@ -30,23 +31,41 @@ export class RetryInterceptor { public static createRetryInterceptor( props: RetryInterceptorProps ): Interceptor { - const logger = props.loggerFactory.getLogger( - RetryInterceptor.constructor.name - ); + const logger = props.loggerFactory.getLogger(RetryInterceptor.name); - const retryStrategy = + const retryStrategy: RetryStrategy = props.retryStrategy ?? new NoRetryStrategy({loggerFactory: props.loggerFactory}); + const overallRequestTimeoutMs = props.overallRequestTimeoutMs; + const deadlineOffset = + retryStrategy.responseDataReceivedTimeoutMillis ?? + props.overallRequestTimeoutMs; + + logger.trace( + `Creating RetryInterceptor (for ${ + props.clientName + }); overall request timeout offset: ${overallRequestTimeoutMs} ms; retry strategy responseDataRecievedTimeoutMillis: ${String( + retryStrategy?.responseDataReceivedTimeoutMillis + )}; deadline offset: ${deadlineOffset} ms` + ); + return (options, nextCall) => { - console.log('THE MAIN RETRY INTERCEPTOR FN IS CALLED'); - if (!options.deadline) { - const deadline = new Date(Date.now()); - deadline.setMilliseconds( - deadline.getMilliseconds() + props.overallRequestTimeoutMs - ); - options.deadline = deadline; - } + logger.trace( + `Entering RetryInterceptor (for ${ + props.clientName + }); overall request timeout offset: ${overallRequestTimeoutMs} ms; deadline offset: ${String( + deadlineOffset + )}` + ); + const overallDeadline = calculateDeadline(overallRequestTimeoutMs); + + logger.trace( + `Setting initial deadline (for ${props.clientName}) based on offset: ${deadlineOffset} ms` + ); + let nextDeadline = calculateDeadline(deadlineOffset); + + options.deadline = nextDeadline; let savedMetadata: Metadata; let savedSendMessage: unknown; @@ -70,16 +89,30 @@ export class RetryInterceptor { next: (arg0: any) => void ) { let attempts = 0; - const originalDeadline = options.deadline; - console.log( - // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - `RETRY INTERCEPTOR: ORIGINAL DEADLINE: ${originalDeadline}` - ); const retry = function (message: unknown, metadata: Metadata) { - console.log( - // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - `RECURSIVE RETRY; ORIGINAL DEADLINE: ${originalDeadline}, NEW DEADLINE: ${options.deadline}` + logger.debug( + `Retrying request: path: ${ + options.method_definition.path + }; deadline was: ${String( + (options.deadline as Date | undefined)?.toISOString() + )}, overall deadline is: ${overallDeadline.toISOString()}` ); + if (new Date(Date.now()) >= overallDeadline) { + logger.debug( + `Request not eligible for retry: path: ${ + options.method_definition.path + }; overall deadline exceeded: ${overallDeadline.toISOString()}` + ); + savedMessageNext(savedReceiveMessage); + next(status); + return; + } + nextDeadline = calculateDeadline(deadlineOffset); + logger.debug( + `Setting next deadline (via offset of ${deadlineOffset} ms) to: ${nextDeadline.toISOString()}` + ); + options.deadline = nextDeadline; + const newCall = nextCall(options); newCall.start(metadata, { onReceiveMessage: function (message) { @@ -152,3 +185,9 @@ export class RetryInterceptor { }; } } + +function calculateDeadline(offsetMillis: number): Date { + const deadline = new Date(Date.now()); + deadline.setMilliseconds(deadline.getMilliseconds() + offsetMillis); + return deadline; +} diff --git a/packages/client-sdk-nodejs/src/internal/internal-auth-client.ts b/packages/client-sdk-nodejs/src/internal/internal-auth-client.ts index 056d8cc3f..a05479c81 100644 --- a/packages/client-sdk-nodejs/src/internal/internal-auth-client.ts +++ b/packages/client-sdk-nodejs/src/internal/internal-auth-client.ts @@ -83,6 +83,7 @@ export class InternalAuthClient implements IAuthClient { this.interceptors = [ HeaderInterceptor.createHeadersInterceptor(headers), RetryInterceptor.createRetryInterceptor({ + clientName: 'AuthClient', loggerFactory: configuration.getLoggerFactory(), overallRequestTimeoutMs: InternalAuthClient.REQUEST_TIMEOUT_MS, }), diff --git a/packages/client-sdk-nodejs/src/internal/leaderboard-data-client.ts b/packages/client-sdk-nodejs/src/internal/leaderboard-data-client.ts index d51e59258..f95b9a34c 100644 --- a/packages/client-sdk-nodejs/src/internal/leaderboard-data-client.ts +++ b/packages/client-sdk-nodejs/src/internal/leaderboard-data-client.ts @@ -144,6 +144,7 @@ export class LeaderboardDataClient implements ILeaderboardDataClient { ), HeaderInterceptor.createHeadersInterceptor(headers), RetryInterceptor.createRetryInterceptor({ + clientName: 'LeaderboardDataClient', loggerFactory: _loggerFactory, overallRequestTimeoutMs: this.requestTimeoutMs, }), diff --git a/packages/client-sdk-nodejs/src/internal/ping-client.ts b/packages/client-sdk-nodejs/src/internal/ping-client.ts index 1fec07f80..9b9292a1b 100644 --- a/packages/client-sdk-nodejs/src/internal/ping-client.ts +++ b/packages/client-sdk-nodejs/src/internal/ping-client.ts @@ -33,6 +33,7 @@ export class InternalNodeGrpcPingClient { this.interceptors = [ HeaderInterceptor.createHeadersInterceptor(headers), RetryInterceptor.createRetryInterceptor({ + clientName: 'PingClient', loggerFactory: props.configuration.getLoggerFactory(), overallRequestTimeoutMs: InternalNodeGrpcPingClient.REQUEST_TIMEOUT_MS, }), diff --git a/packages/client-sdk-nodejs/src/internal/pubsub-client.ts b/packages/client-sdk-nodejs/src/internal/pubsub-client.ts index fa067ddc3..60d51c460 100644 --- a/packages/client-sdk-nodejs/src/internal/pubsub-client.ts +++ b/packages/client-sdk-nodejs/src/internal/pubsub-client.ts @@ -301,6 +301,7 @@ export class PubsubClient extends AbstractPubsubClient { middlewaresInterceptor(configuration.getLoggerFactory(), [], {}), HeaderInterceptor.createHeadersInterceptor(headers), RetryInterceptor.createRetryInterceptor({ + clientName: 'PubSubClient', loggerFactory: configuration.getLoggerFactory(), overallRequestTimeoutMs: requestTimeoutMs, }), diff --git a/packages/client-sdk-nodejs/src/internal/storage-control-client.ts b/packages/client-sdk-nodejs/src/internal/storage-control-client.ts index 332535846..3056adcb3 100644 --- a/packages/client-sdk-nodejs/src/internal/storage-control-client.ts +++ b/packages/client-sdk-nodejs/src/internal/storage-control-client.ts @@ -31,6 +31,7 @@ export class StorageControlClient { this.interceptors = [ HeaderInterceptor.createHeadersInterceptor(headers), RetryInterceptor.createRetryInterceptor({ + clientName: 'StorageControlClient', loggerFactory: props.configuration.getLoggerFactory(), overallRequestTimeoutMs: StorageControlClient.REQUEST_TIMEOUT_MS, }), diff --git a/packages/client-sdk-nodejs/src/internal/storage-data-client.ts b/packages/client-sdk-nodejs/src/internal/storage-data-client.ts index 115736ef1..c1113c352 100644 --- a/packages/client-sdk-nodejs/src/internal/storage-data-client.ts +++ b/packages/client-sdk-nodejs/src/internal/storage-data-client.ts @@ -51,7 +51,7 @@ export class StorageDataClient implements IStorageDataClient { .getDeadlineMillis(); this.validateRequestTimeout(this.requestTimeoutMs); this.logger.debug( - `Creating leaderboard client using endpoint: '${this.credentialProvider.getStorageEndpoint()}'` + `Creating storage client using endpoint: '${this.credentialProvider.getStorageEndpoint()}'` ); // NOTE: This is hard-coded for now but we may want to expose it via StorageConfiguration in the @@ -111,10 +111,10 @@ export class StorageDataClient implements IStorageDataClient { new Header('agent', `nodejs:store:${version}`), new Header('runtime-version', `nodejs:${process.versions.node}`), ]; - return [ HeaderInterceptor.createHeadersInterceptor(headers), RetryInterceptor.createRetryInterceptor({ + clientName: 'StorageDataClient', loggerFactory: this.configuration.getLoggerFactory(), retryStrategy: this.configuration.getRetryStrategy(), overallRequestTimeoutMs: this.requestTimeoutMs, diff --git a/packages/client-sdk-nodejs/src/internal/webhook-client.ts b/packages/client-sdk-nodejs/src/internal/webhook-client.ts index 402774d3c..476be8322 100644 --- a/packages/client-sdk-nodejs/src/internal/webhook-client.ts +++ b/packages/client-sdk-nodejs/src/internal/webhook-client.ts @@ -50,6 +50,7 @@ export class WebhookClient implements IWebhookClient { this.unaryInterceptors = [ HeaderInterceptor.createHeadersInterceptor(headers), RetryInterceptor.createRetryInterceptor({ + clientName: 'WebhookClient', loggerFactory: props.configuration.getLoggerFactory(), overallRequestTimeoutMs: WebhookClient.DEFAULT_REQUEST_TIMEOUT_MS, }), From 57a0e925a4df0ba1dc5a13f6672e42b3c65cf3aa Mon Sep 17 00:00:00 2001 From: Chris Price Date: Tue, 27 Aug 2024 13:22:23 -0700 Subject: [PATCH 43/43] chore: remove accidental momento local in storage basic example --- examples/nodejs/storage/basic.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/nodejs/storage/basic.ts b/examples/nodejs/storage/basic.ts index 005ecfe27..cf81d325f 100644 --- a/examples/nodejs/storage/basic.ts +++ b/examples/nodejs/storage/basic.ts @@ -10,7 +10,7 @@ import { async function main() { const storageClient = new PreviewStorageClient({ configuration: StorageConfigurations.Laptop.latest(), - credentialProvider: CredentialProvider.fromEnvironmentVariable('MOMENTO_API_KEY').withMomentoLocal(), + credentialProvider: CredentialProvider.fromEnvironmentVariable('MOMENTO_API_KEY'), }); const storeName = 'my-store';