diff --git a/.cspell.json b/.cspell.json index 11f6738cf09..e162b3023c1 100644 --- a/.cspell.json +++ b/.cspell.json @@ -518,6 +518,9 @@ "idempotency", "IDEMPOTENCY", "Idempotency", + "Retryable", + "RETRYABLE", + "retryable", "messagebird", "Datetime", "pubid", diff --git a/packages/node/package.json b/packages/node/package.json index 45b76671a4e..193bab03ab0 100644 --- a/packages/node/package.json +++ b/packages/node/package.json @@ -43,9 +43,11 @@ }, "dependencies": { "@novu/shared": "^0.21.0", + "axios-retry": "^3.8.0", "handlebars": "^4.7.7", "lodash.get": "^4.4.2", - "lodash.merge": "^4.6.2" + "lodash.merge": "^4.6.2", + "uuid": "^9.0.1" }, "devDependencies": { "@sendgrid/mail": "^7.4.6", @@ -53,9 +55,11 @@ "@types/lodash.get": "^4.4.6", "@types/lodash.merge": "^4.6.6", "@types/node": "^14.6.0", + "@types/uuid": "^8.3.4", "axios": "^1.4.0", "codecov": "^3.5.0", "jest": "^27.0.6", + "nock": "^13.1.3", "npm-run-all": "^4.1.5", "open-cli": "^6.0.1", "rimraf": "^3.0.2", @@ -77,7 +81,7 @@ "preset": "ts-jest", "testEnvironment": "node", "moduleNameMapper": { - "axios": "axios/dist/node/axios.cjs" + "^axios$": "axios/dist/node/axios.cjs" } }, "prettier": { diff --git a/packages/node/src/index.ts b/packages/node/src/index.ts index 64cbe3e9569..e1104a7e0de 100644 --- a/packages/node/src/index.ts +++ b/packages/node/src/index.ts @@ -30,3 +30,4 @@ export * from './lib/feeds/feeds.interface'; export * from './lib/topics/topic.interface'; export * from './lib/integrations/integrations.interface'; export * from './lib/messages/messages.interface'; +export { defaultRetryCondition } from './lib/retry'; diff --git a/packages/node/src/lib/novu.interface.ts b/packages/node/src/lib/novu.interface.ts index 95b8e4e69e5..953238f47ec 100644 --- a/packages/node/src/lib/novu.interface.ts +++ b/packages/node/src/lib/novu.interface.ts @@ -1,7 +1,16 @@ -import { AxiosInstance } from 'axios'; +import { AxiosError, AxiosInstance } from 'axios'; + +export interface IRetryConfig { + initialDelay?: number; + waitMin?: number; + waitMax?: number; + retryMax?: number; + retryCondition?: (err: AxiosError) => boolean; +} export interface INovuConfiguration { backendUrl?: string; + retryConfig?: IRetryConfig; } export class WithHttp { diff --git a/packages/node/src/lib/novu.ts b/packages/node/src/lib/novu.ts index 5d721db6807..1cb15573088 100644 --- a/packages/node/src/lib/novu.ts +++ b/packages/node/src/lib/novu.ts @@ -13,6 +13,7 @@ import { Topics } from './topics/topics'; import { Integrations } from './integrations/integrations'; import { Messages } from './messages/messages'; import { Tenants } from './tenants/tenants'; +import { makeRetryable } from './retry'; export class Novu extends EventEmitter { private readonly apiKey?: string; @@ -33,14 +34,19 @@ export class Novu extends EventEmitter { constructor(apiKey: string, config?: INovuConfiguration) { super(); this.apiKey = apiKey; - - this.http = axios.create({ + const axiosInstance = axios.create({ baseURL: this.buildBackendUrl(config), headers: { Authorization: `ApiKey ${this.apiKey}`, }, }); + if (config?.retryConfig) { + makeRetryable(axiosInstance, config); + } + + this.http = axiosInstance; + this.subscribers = new Subscribers(this.http); this.environments = new Environments(this.http); this.events = new Events(this.http); diff --git a/packages/node/src/lib/retry.spec.ts b/packages/node/src/lib/retry.spec.ts new file mode 100644 index 00000000000..19270066d10 --- /dev/null +++ b/packages/node/src/lib/retry.spec.ts @@ -0,0 +1,275 @@ +import { AxiosError } from 'axios'; +import nock from 'nock'; +import { Novu, defaultRetryCondition } from '../index'; +import { RETRYABLE_HTTP_CODES } from './retry'; + +const BACKEND_URL = 'http://example.com'; +const TOPICS_PATH = '/v1/topics'; +const TRIGGER_PATH = '/v1/events/trigger'; + +jest.setTimeout(15000); + +const hasAllEqual = (arr: Array) => arr.every((val) => val === arr[0]); +const hasUniqueOnly = (arr: Array) => + Array.from(new Set(arr)).length === arr.length; + +class NetworkError extends Error { + constructor(public code: string) { + super('Network Error'); + } +} + +class HttpError extends Error { + readonly response: { status: number }; + + constructor(status: number) { + super('Http Error'); + this.response = { status }; + } +} + +describe('Novu Node.js package - Retries and idempotency-key', () => { + afterEach(() => { + nock.cleanAll(); + nock.enableNetConnect(); + }); + + const novu = new Novu('fake-key', { + backendUrl: BACKEND_URL, + retryConfig: { + retryMax: 3, + waitMax: 0.5, + waitMin: 0.2, + }, + }); + + it('should retry trigger and generate idempotency-key only once for request', async () => { + const idempotencyKeys: string[] = []; + + nock(BACKEND_URL) + .post(TRIGGER_PATH) + .times(3) + .reply(function (_url, _body) { + idempotencyKeys.push(this.req.getHeader('idempotency-key') as string); + + return [500, { message: 'Server Exception' }]; + }); + + nock(BACKEND_URL) + .post(TRIGGER_PATH) + .reply(201, { acknowledged: true, transactionId: '1003' }); + + const result = await novu.trigger('fake-workflow', { + to: { subscriberId: '123' }, + payload: {}, + }); + + // all idempotency keys are supposed to be same. + expect(hasAllEqual(idempotencyKeys)).toEqual(true); + expect(result.status).toEqual(201); + expect(result.request.headers['idempotency-key']).toBeDefined(); + }); + + it('should generate different idempotency-key for each request', async () => { + nock(BACKEND_URL) + .post(TRIGGER_PATH) + .reply(500, { message: 'Server Exception' }); + + nock(BACKEND_URL) + .post(TRIGGER_PATH) + .times(10) + .reply(201, { acknowledged: true, transactionId: '1003' }); + + const idempotencyKeys: string[] = []; + + for (let i = 0; i < 10; i++) { + const result = await novu.trigger('fake-workflow', { + to: { subscriberId: '123' }, + payload: {}, + }); + + idempotencyKeys.push(result.request?.headers['idempotency-key']); + } + + expect(hasUniqueOnly(idempotencyKeys)).toEqual(true); + }); + + it('should retry on status 422 and regenerate idempotency-key for every retry', async () => { + const idempotencyKeys: string[] = []; + + nock(BACKEND_URL) + .post(TRIGGER_PATH) + .times(3) + .reply(function () { + idempotencyKeys.push(this.req.getHeader('idempotency-key') as string); + + return [422, { message: 'Unprocessable Content' }]; + }); + + nock(BACKEND_URL) + .post(TRIGGER_PATH) + .reply(201, { acknowledged: true, transactionId: '1003' }); + + const result = await novu.trigger('fake-workflow', { + to: { subscriberId: '123' }, + payload: {}, + }); + + // idempotency key should be regenerated for every retry for http status 422. + expect(hasUniqueOnly(idempotencyKeys)).toEqual(true); + expect(result.status).toEqual(201); + expect(result.request.headers['idempotency-key']).toBeDefined(); + }); + + it('should retry getting topics list', async () => { + nock(BACKEND_URL) + .get(TOPICS_PATH) + .times(3) + .reply(500, { message: 'Server Exception' }); + + nock(BACKEND_URL).get(TOPICS_PATH).reply(200, [{}, {}]); + + const result = await novu.topics.list({}); + + expect(result.status).toEqual(200); + expect(result.request.headers['idempotency-key']).toBeUndefined(); + }); + + it('should fail after reaching max retries', async () => { + nock(BACKEND_URL) + .get(TOPICS_PATH) + .times(4) + .reply(500, { message: 'Server Exception' }); + + nock(BACKEND_URL).get(TOPICS_PATH).reply(200, [{}, {}]); + + await expect(novu.topics.list({})).rejects.toMatchObject({ + response: { status: 500 }, + }); + }); + + const NON_RECOVERABLE_ERRORS: Array<[number, string]> = [ + [400, 'Bad Request'], + [401, 'Unauthorized'], + [403, 'Forbidden'], + [404, 'Not Found'], + [405, 'Method not allowed'], + [413, 'Payload Too Large'], + [414, 'URI Too Long'], + [415, 'Unsupported Media Type'], + ]; + + test.each<[number, string]>(NON_RECOVERABLE_ERRORS)( + 'should not retry on non-recoverable %i error', + async (status, message) => { + nock(BACKEND_URL).get(TOPICS_PATH).times(3).reply(status, { message }); + nock(BACKEND_URL).get(TOPICS_PATH).reply(200, [{}, {}]); + + await expect(novu.topics.list({})).rejects.toMatchObject({ + response: { status }, + }); + } + ); + + it('should retry on various errors until it reach successful response', async () => { + nock(BACKEND_URL) + .get(TOPICS_PATH) + .reply(429, { message: 'Too many requests' }); + + nock(BACKEND_URL) + .get(TOPICS_PATH) + .reply(408, { message: 'Request Timeout' }); + + nock(BACKEND_URL) + .get(TOPICS_PATH) + .replyWithError(new NetworkError('ECONNRESET')); + + nock(BACKEND_URL) + .get(TOPICS_PATH) + .replyWithError(new NetworkError('ETIMEDOUT')); + + nock(BACKEND_URL) + .get(TOPICS_PATH) + .replyWithError(new NetworkError('ECONNREFUSED')); + + nock(BACKEND_URL) + .get(TOPICS_PATH) + .reply(504, { message: 'Gateway timeout' }); + + nock(BACKEND_URL) + .get(TOPICS_PATH) + .reply(422, { message: 'Unprocessable Content' }); + + nock(BACKEND_URL).get(TOPICS_PATH).reply(200, [{}, {}]); + + const novuClient = new Novu('fake-key', { + backendUrl: BACKEND_URL, + retryConfig: { + initialDelay: 0, + waitMin: 0.2, + waitMax: 0.5, + retryMax: 7, + }, + }); + + const result = await novuClient.topics.list({}); + expect(result.status).toEqual(200); + }); + + describe('defaultRetryCondition function', () => { + test.each<[number, string]>(NON_RECOVERABLE_ERRORS)( + 'should return false when HTTP status is %i', + (status) => { + const err = new HttpError(status); + expect(defaultRetryCondition(err as AxiosError)).toEqual(false); + } + ); + + test.each(RETRYABLE_HTTP_CODES)( + 'should return true when HTTP status is %i', + (status) => { + const err = new HttpError(status); + expect(defaultRetryCondition(err as AxiosError)).toEqual(true); + } + ); + + it('should return true when HTTP status is 500', () => { + const err = new HttpError(500); + expect(defaultRetryCondition(err as AxiosError)).toEqual(true); + }); + + it('should return true when network code is ECONNRESET', () => { + const err = new NetworkError('ECONNRESET'); + expect(defaultRetryCondition(err as AxiosError)).toEqual(true); + }); + + it('shoud return false on unknown error', () => { + const err = new Error('Unexpected error'); + expect(defaultRetryCondition(err as AxiosError)).toEqual(false); + }); + }); + + describe('hasAllEqual helper function', () => { + it('should return true when all items are equal', () => { + const arr = ['a', 'a', 'a', 'a']; + expect(hasAllEqual(arr)).toEqual(true); + }); + + it('should return false when items are not equal', () => { + const arr = ['a', 'b', 'b', 'b']; + expect(hasAllEqual(arr)).toEqual(false); + }); + }); + + describe('hasUniqueOnly helper function', () => { + it('should return true when all items are unique', () => { + const arr = ['a', 'b', 'c', 'd']; + expect(hasUniqueOnly(arr)).toEqual(true); + }); + + it('should return false when items are not unique', () => { + const arr = ['a', 'a', 'c', 'd']; + expect(hasUniqueOnly(arr)).toEqual(false); + }); + }); +}); diff --git a/packages/node/src/lib/retry.ts b/packages/node/src/lib/retry.ts new file mode 100644 index 00000000000..22d79fe4a8a --- /dev/null +++ b/packages/node/src/lib/retry.ts @@ -0,0 +1,93 @@ +import { AxiosError, AxiosInstance } from 'axios'; +import axiosRetry, { isNetworkError } from 'axios-retry'; +import { v4 as uuid } from 'uuid'; +import { INovuConfiguration } from './novu.interface'; + +export const RETRYABLE_HTTP_CODES = [408, 422, 429]; +const NON_IDEMPOTENT_METHODS = ['post', 'patch']; +const IDEMPOTENCY_KEY = 'Idempotency-Key'; // header key + +const DEFAULT_RETRY_MAX = 0; +const DEFAULT_WAIT_MIN = 1; +const DEFAULT_WAIT_MAX = 30; + +export function makeRetryable( + axios: AxiosInstance, + config?: INovuConfiguration +) { + axios.interceptors.request.use((axiosConfig) => { + if ( + axiosConfig.method && + NON_IDEMPOTENT_METHODS.includes(axiosConfig.method) + ) { + const idempotencyKey = axiosConfig.headers[IDEMPOTENCY_KEY]; + // that means intercepted request is retried, so don't generate new idempotency key + if (idempotencyKey) { + return axiosConfig; + } + + axiosConfig.headers[IDEMPOTENCY_KEY] = uuid(); + } + + return axiosConfig; + }); + + const retryConfig = config?.retryConfig || {}; + const retries = retryConfig.retryMax || DEFAULT_RETRY_MAX; + const minDelay = retryConfig.waitMin || DEFAULT_WAIT_MIN; + const maxDelay = retryConfig.waitMax || DEFAULT_WAIT_MAX; + const initialDelay = retryConfig.initialDelay || minDelay; + const retryCondition = retryConfig.retryCondition || defaultRetryCondition; + + function backoff(retryCount: number) { + if (retryCount === 1) { + return initialDelay; + } + + const delay = retryCount * minDelay; + if (delay > maxDelay) { + return maxDelay; + } + + return delay; + } + + axiosRetry(axios, { + retries, + retryCondition, + retryDelay(retryCount) { + return backoff(retryCount) * 1000; // return delay in milliseconds + }, + onRetry(_retryCount, error, requestConfig) { + if ( + error.response?.status === 422 && + requestConfig.headers && + requestConfig.method && + NON_IDEMPOTENT_METHODS.includes(requestConfig.method) + ) { + requestConfig.headers[IDEMPOTENCY_KEY] = uuid(); + } + }, + }); +} + +export function defaultRetryCondition(err: AxiosError): boolean { + // retry on TCP/IP error codes like ECONNRESET + if (isNetworkError(err)) { + return true; + } + + if ( + err.response && + err.response.status >= 500 && + err.response.status <= 599 + ) { + return true; + } + + if (err.response && RETRYABLE_HTTP_CODES.includes(err.response.status)) { + return true; + } + + return false; +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e83b36ef120..a1d31629518 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2601,6 +2601,9 @@ importers: '@novu/shared': specifier: ^0.21.0 version: link:../../libs/shared + axios-retry: + specifier: ^3.8.0 + version: 3.8.1 handlebars: specifier: ^4.7.7 version: 4.7.7 @@ -2610,6 +2613,9 @@ importers: lodash.merge: specifier: ^4.6.2 version: 4.6.2 + uuid: + specifier: ^9.0.1 + version: 9.0.1 devDependencies: '@sendgrid/mail': specifier: ^7.4.6 @@ -2626,6 +2632,9 @@ importers: '@types/node': specifier: ^14.6.0 version: 14.18.42 + '@types/uuid': + specifier: ^8.3.4 + version: 8.3.4 axios: specifier: ^1.4.0 version: 1.4.0 @@ -2635,6 +2644,9 @@ importers: jest: specifier: ^27.0.6 version: 27.5.1(ts-node@10.9.1) + nock: + specifier: ^13.1.3 + version: 13.3.0 npm-run-all: specifier: ^4.1.5 version: 4.1.5 @@ -6474,7 +6486,7 @@ packages: peerDependencies: react: '>=16.9.0' dependencies: - '@babel/runtime': 7.21.0 + '@babel/runtime': 7.23.2 classnames: 2.3.2 json2mq: 0.2.0 lodash: 4.17.21 @@ -18643,7 +18655,7 @@ packages: peerDependencies: react: ^16.8 || ^17.0 dependencies: - '@babel/runtime': 7.21.0 + '@babel/runtime': 7.23.2 '@radix-ui/number': 0.1.0 '@radix-ui/primitive': 0.1.0 '@radix-ui/react-compose-refs': 0.1.0(react@17.0.2) @@ -18662,7 +18674,7 @@ packages: react: ^16.8 || ^17.0 || ^18.0 react-dom: ^16.8 || ^17.0 || ^18.0 dependencies: - '@babel/runtime': 7.21.0 + '@babel/runtime': 7.23.2 '@radix-ui/number': 1.0.0 '@radix-ui/primitive': 1.0.0 '@radix-ui/react-compose-refs': 1.0.0(react@17.0.2) @@ -23289,7 +23301,7 @@ packages: engines: {node: '>=10'} dependencies: '@babel/code-frame': 7.22.13 - '@babel/runtime': 7.21.0 + '@babel/runtime': 7.23.2 '@types/aria-query': 4.2.2 aria-query: 4.2.2 chalk: 4.1.2 @@ -26621,6 +26633,13 @@ packages: is-retry-allowed: 1.2.0 dev: false + /axios-retry@3.8.1: + resolution: {integrity: sha512-4XseuArB4CEbfLRtMpUods2q8MLBvD4r8ifKgK4SP2FRgzQIPUDpzZ+cjQ/19eu3w2UpKgkJA+myEh2BYDSjqQ==} + dependencies: + '@babel/runtime': 7.23.2 + is-retry-allowed: 2.2.0 + dev: false + /axios@0.21.4: resolution: {integrity: sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==} dependencies: @@ -26899,7 +26918,7 @@ packages: resolution: {integrity: sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==} engines: {node: '>=10', npm: '>=6'} dependencies: - '@babel/runtime': 7.21.0 + '@babel/runtime': 7.23.2 cosmiconfig: 7.1.0 resolve: 1.22.2 @@ -31488,7 +31507,7 @@ packages: peerDependencies: eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 dependencies: - '@babel/runtime': 7.21.0 + '@babel/runtime': 7.23.2 aria-query: 5.1.3 array-includes: 3.1.6 array.prototype.flatmap: 1.3.1 @@ -32700,7 +32719,7 @@ packages: jsonwebtoken: 9.0.0 jwks-rsa: 3.0.1 node-forge: 1.3.1 - uuid: 9.0.0 + uuid: 9.0.1 optionalDependencies: '@google-cloud/firestore': 6.8.0 '@google-cloud/storage': 6.12.0 @@ -35398,6 +35417,11 @@ packages: engines: {node: '>=0.10.0'} dev: false + /is-retry-allowed@2.2.0: + resolution: {integrity: sha512-XVm7LOeLpTW4jV19QSH38vkswxoLud8sQ57YwJVTPWdiaI9I8keEhGFpBlslyVsgdQy4Opg8QOLb8YRgsyZiQg==} + engines: {node: '>=10'} + dev: false + /is-root@2.1.0: resolution: {integrity: sha512-AGOriNp96vNBd3HtU+RzFEc75FfR5ymiYv8E553I71SCeXBiMsVDUtdio1OEFvrPyLIQ9tVR5RxXIFe5PUFjMg==} engines: {node: '>=6'} @@ -39853,7 +39877,6 @@ packages: propagate: 2.0.1 transitivePeerDependencies: - supports-color - dev: false /node-abort-controller@3.1.1: resolution: {integrity: sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==} @@ -42920,7 +42943,6 @@ packages: /propagate@2.0.1: resolution: {integrity: sha512-vGrhOavPSTz4QVNuBNdcNXePNdNMaO1xj9yBeH1ScQPjk/rhg9sSlCXPhMkFuaNNW/syTvYqsnbIJxMBfRbbag==} engines: {node: '>= 8'} - dev: false /property-information@5.6.0: resolution: {integrity: sha512-YUHSPk+A30YPv+0Qf8i9Mbfe/C0hdPXk1s1jPVToV8pk8BQtpw10ct89Eo7OWkutrwqvT0eicAxlOg3dOAu8JA==} @@ -43254,7 +43276,7 @@ packages: react: '>=16.9.0' react-dom: '>=16.9.0' dependencies: - '@babel/runtime': 7.21.0 + '@babel/runtime': 7.23.2 array-tree-filter: 2.1.0 classnames: 2.3.2 rc-select: 14.1.17(react-dom@17.0.2)(react@17.0.2) @@ -43270,7 +43292,7 @@ packages: react: '>=16.9.0' react-dom: '>=16.9.0' dependencies: - '@babel/runtime': 7.21.0 + '@babel/runtime': 7.23.2 classnames: 2.3.2 react: 17.0.2 react-dom: 17.0.2(react@17.0.2) @@ -43282,7 +43304,7 @@ packages: react: '>=16.9.0' react-dom: '>=16.9.0' dependencies: - '@babel/runtime': 7.21.0 + '@babel/runtime': 7.23.2 classnames: 2.3.2 rc-motion: 2.6.3(react-dom@17.0.2)(react@17.0.2) rc-util: 5.29.3(react-dom@17.0.2)(react@17.0.2) @@ -43297,7 +43319,7 @@ packages: react: '>=16.9.0' react-dom: '>=16.9.0' dependencies: - '@babel/runtime': 7.21.0 + '@babel/runtime': 7.23.2 '@rc-component/portal': 1.1.1(react-dom@17.0.2)(react@17.0.2) classnames: 2.3.2 rc-motion: 2.6.3(react-dom@17.0.2)(react@17.0.2) @@ -43312,7 +43334,7 @@ packages: react: '>=16.9.0' react-dom: '>=16.9.0' dependencies: - '@babel/runtime': 7.21.0 + '@babel/runtime': 7.23.2 '@rc-component/portal': 1.1.1(react-dom@17.0.2)(react@17.0.2) classnames: 2.3.2 rc-motion: 2.6.3(react-dom@17.0.2)(react@17.0.2) @@ -43327,7 +43349,7 @@ packages: react: '>=16.11.0' react-dom: '>=16.11.0' dependencies: - '@babel/runtime': 7.21.0 + '@babel/runtime': 7.23.2 classnames: 2.3.2 rc-trigger: 5.3.4(react-dom@17.0.2)(react@17.0.2) rc-util: 5.29.3(react-dom@17.0.2)(react@17.0.2) @@ -43342,7 +43364,7 @@ packages: react: '>=16.9.0' react-dom: '>=16.9.0' dependencies: - '@babel/runtime': 7.21.0 + '@babel/runtime': 7.23.2 async-validator: 4.2.5 rc-util: 5.29.3(react-dom@17.0.2)(react@17.0.2) react: 17.0.2 @@ -43355,7 +43377,7 @@ packages: react: '>=16.9.0' react-dom: '>=16.9.0' dependencies: - '@babel/runtime': 7.21.0 + '@babel/runtime': 7.23.2 '@rc-component/portal': 1.1.1(react-dom@17.0.2)(react@17.0.2) classnames: 2.3.2 rc-dialog: 9.0.2(react-dom@17.0.2)(react@17.0.2) @@ -43371,7 +43393,7 @@ packages: react: '>=16.9.0' react-dom: '>=16.9.0' dependencies: - '@babel/runtime': 7.21.0 + '@babel/runtime': 7.23.2 classnames: 2.3.2 rc-util: 5.29.3(react-dom@17.0.2)(react@17.0.2) react: 17.0.2 @@ -43384,7 +43406,7 @@ packages: react: '>=16.0.0' react-dom: '>=16.0.0' dependencies: - '@babel/runtime': 7.21.0 + '@babel/runtime': 7.23.2 classnames: 2.3.2 rc-util: 5.29.3(react-dom@17.0.2)(react@17.0.2) react: 17.0.2 @@ -43397,7 +43419,7 @@ packages: react: '>=16.9.0' react-dom: '>=16.9.0' dependencies: - '@babel/runtime': 7.21.0 + '@babel/runtime': 7.23.2 classnames: 2.3.2 rc-menu: 9.8.4(react-dom@17.0.2)(react@17.0.2) rc-textarea: 0.4.7(react-dom@17.0.2)(react@17.0.2) @@ -43413,7 +43435,7 @@ packages: react: '>=16.9.0' react-dom: '>=16.9.0' dependencies: - '@babel/runtime': 7.21.0 + '@babel/runtime': 7.23.2 classnames: 2.3.2 rc-motion: 2.6.3(react-dom@17.0.2)(react@17.0.2) rc-overflow: 1.3.0(react-dom@17.0.2)(react@17.0.2) @@ -43429,7 +43451,7 @@ packages: react: '>=16.9.0' react-dom: '>=16.9.0' dependencies: - '@babel/runtime': 7.21.0 + '@babel/runtime': 7.23.2 classnames: 2.3.2 rc-util: 5.29.3(react-dom@17.0.2)(react@17.0.2) react: 17.0.2 @@ -43443,7 +43465,7 @@ packages: react: '>=16.9.0' react-dom: '>=16.9.0' dependencies: - '@babel/runtime': 7.21.0 + '@babel/runtime': 7.23.2 classnames: 2.3.2 rc-motion: 2.6.3(react-dom@17.0.2)(react@17.0.2) rc-util: 5.29.3(react-dom@17.0.2)(react@17.0.2) @@ -43471,7 +43493,7 @@ packages: react: '>=16.9.0' react-dom: '>=16.9.0' dependencies: - '@babel/runtime': 7.21.0 + '@babel/runtime': 7.23.2 classnames: 2.3.2 react: 17.0.2 react-dom: 17.0.2(react@17.0.2) @@ -43484,7 +43506,7 @@ packages: react: '>=16.9.0' react-dom: '>=16.9.0' dependencies: - '@babel/runtime': 7.21.0 + '@babel/runtime': 7.23.2 classnames: 2.3.2 date-fns: 2.29.3 dayjs: 1.11.9 @@ -43502,7 +43524,7 @@ packages: react: '>=16.9.0' react-dom: '>=16.9.0' dependencies: - '@babel/runtime': 7.21.0 + '@babel/runtime': 7.23.2 classnames: 2.3.2 rc-util: 5.29.3(react-dom@17.0.2)(react@17.0.2) react: 17.0.2 @@ -43516,7 +43538,7 @@ packages: react: '>=16.9.0' react-dom: '>=16.9.0' dependencies: - '@babel/runtime': 7.21.0 + '@babel/runtime': 7.23.2 classnames: 2.3.2 rc-util: 5.29.3(react-dom@17.0.2)(react@17.0.2) react: 17.0.2 @@ -43529,7 +43551,7 @@ packages: react: '>=16.9.0' react-dom: '>=16.9.0' dependencies: - '@babel/runtime': 7.21.0 + '@babel/runtime': 7.23.2 classnames: 2.3.2 rc-util: 5.29.3(react-dom@17.0.2)(react@17.0.2) react: 17.0.2 @@ -43543,7 +43565,7 @@ packages: react: '>=16.0.0' react-dom: '>=16.0.0' dependencies: - '@babel/runtime': 7.21.0 + '@babel/runtime': 7.23.2 classnames: 2.3.2 rc-motion: 2.6.3(react-dom@17.0.2)(react@17.0.2) rc-util: 5.29.3(react-dom@17.0.2)(react@17.0.2) @@ -43558,7 +43580,7 @@ packages: react: '*' react-dom: '*' dependencies: - '@babel/runtime': 7.21.0 + '@babel/runtime': 7.23.2 classnames: 2.3.2 rc-motion: 2.6.3(react-dom@17.0.2)(react@17.0.2) rc-overflow: 1.3.0(react-dom@17.0.2)(react@17.0.2) @@ -43576,7 +43598,7 @@ packages: react: '>=16.9.0' react-dom: '>=16.9.0' dependencies: - '@babel/runtime': 7.21.0 + '@babel/runtime': 7.23.2 classnames: 2.3.2 rc-util: 5.29.3(react-dom@17.0.2)(react@17.0.2) react: 17.0.2 @@ -43591,7 +43613,7 @@ packages: react: '>=16.9.0' react-dom: '>=16.9.0' dependencies: - '@babel/runtime': 7.21.0 + '@babel/runtime': 7.23.2 classnames: 2.3.2 rc-util: 5.29.3(react-dom@17.0.2)(react@17.0.2) react: 17.0.2 @@ -43604,7 +43626,7 @@ packages: react: '>=16.9.0' react-dom: '>=16.9.0' dependencies: - '@babel/runtime': 7.21.0 + '@babel/runtime': 7.23.2 classnames: 2.3.2 rc-util: 5.29.3(react-dom@17.0.2)(react@17.0.2) react: 17.0.2 @@ -43618,7 +43640,7 @@ packages: react: '>=16.9.0' react-dom: '>=16.9.0' dependencies: - '@babel/runtime': 7.21.0 + '@babel/runtime': 7.23.2 classnames: 2.3.2 rc-resize-observer: 1.3.1(react-dom@17.0.2)(react@17.0.2) rc-util: 5.29.3(react-dom@17.0.2)(react@17.0.2) @@ -43634,7 +43656,7 @@ packages: react: '>=16.9.0' react-dom: '>=16.9.0' dependencies: - '@babel/runtime': 7.21.0 + '@babel/runtime': 7.23.2 classnames: 2.3.2 rc-dropdown: 4.0.1(react-dom@17.0.2)(react@17.0.2) rc-menu: 9.8.4(react-dom@17.0.2)(react@17.0.2) @@ -43651,7 +43673,7 @@ packages: react: '>=16.9.0' react-dom: '>=16.9.0' dependencies: - '@babel/runtime': 7.21.0 + '@babel/runtime': 7.23.2 classnames: 2.3.2 rc-resize-observer: 1.3.1(react-dom@17.0.2)(react@17.0.2) rc-util: 5.29.3(react-dom@17.0.2)(react@17.0.2) @@ -43666,7 +43688,7 @@ packages: react: '>=16.9.0' react-dom: '>=16.9.0' dependencies: - '@babel/runtime': 7.21.0 + '@babel/runtime': 7.23.2 classnames: 2.3.2 rc-trigger: 5.3.4(react-dom@17.0.2)(react@17.0.2) react: 17.0.2 @@ -43679,7 +43701,7 @@ packages: react: '*' react-dom: '*' dependencies: - '@babel/runtime': 7.21.0 + '@babel/runtime': 7.23.2 classnames: 2.3.2 rc-select: 14.1.17(react-dom@17.0.2)(react@17.0.2) rc-tree: 5.7.3(react-dom@17.0.2)(react@17.0.2) @@ -43695,7 +43717,7 @@ packages: react: '*' react-dom: '*' dependencies: - '@babel/runtime': 7.21.0 + '@babel/runtime': 7.23.2 classnames: 2.3.2 rc-motion: 2.6.3(react-dom@17.0.2)(react@17.0.2) rc-util: 5.29.3(react-dom@17.0.2)(react@17.0.2) @@ -43711,7 +43733,7 @@ packages: react: '>=16.9.0' react-dom: '>=16.9.0' dependencies: - '@babel/runtime': 7.21.0 + '@babel/runtime': 7.23.2 classnames: 2.3.2 rc-align: 4.0.15(react-dom@17.0.2)(react@17.0.2) rc-motion: 2.6.3(react-dom@17.0.2)(react@17.0.2) @@ -43726,7 +43748,7 @@ packages: react: '>=16.9.0' react-dom: '>=16.9.0' dependencies: - '@babel/runtime': 7.21.0 + '@babel/runtime': 7.23.2 classnames: 2.3.2 rc-util: 5.29.3(react-dom@17.0.2)(react@17.0.2) react: 17.0.2 @@ -43739,7 +43761,7 @@ packages: react: '>=16.9.0' react-dom: '>=16.9.0' dependencies: - '@babel/runtime': 7.21.0 + '@babel/runtime': 7.23.2 react: 17.0.2 react-dom: 17.0.2(react@17.0.2) react-is: 16.13.1 @@ -44522,7 +44544,7 @@ packages: peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 dependencies: - '@babel/runtime': 7.21.0 + '@babel/runtime': 7.23.2 react: 17.0.2 use-composed-ref: 1.3.0(react@17.0.2) use-latest: 1.2.1(@types/react@17.0.53)(react@17.0.2) @@ -44536,7 +44558,7 @@ packages: peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 dependencies: - '@babel/runtime': 7.21.0 + '@babel/runtime': 7.23.2 react: 17.0.2 use-composed-ref: 1.3.0(react@17.0.2) use-latest: 1.2.1(@types/react@17.0.62)(react@17.0.2) @@ -44550,7 +44572,7 @@ packages: peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 dependencies: - '@babel/runtime': 7.21.0 + '@babel/runtime': 7.23.2 react: 17.0.2 use-composed-ref: 1.3.0(react@17.0.2) use-latest: 1.2.1(@types/react@17.0.62)(react@17.0.2) @@ -44564,7 +44586,7 @@ packages: react: '>=16.6.0' react-dom: '>=16.6.0' dependencies: - '@babel/runtime': 7.21.0 + '@babel/runtime': 7.23.2 dom-helpers: 5.2.1 loose-envify: 1.4.0 prop-types: 15.8.1 @@ -47493,7 +47515,7 @@ packages: https-proxy-agent: 5.0.1 node-fetch: 2.6.9 stream-events: 1.0.5 - uuid: 9.0.0 + uuid: 9.0.1 transitivePeerDependencies: - encoding - supports-color @@ -49116,7 +49138,7 @@ packages: peerDependencies: react: '>=16.13' dependencies: - '@babel/runtime': 7.21.0 + '@babel/runtime': 7.23.2 dequal: 2.0.3 react: 17.0.2 dev: false @@ -49300,6 +49322,11 @@ packages: resolution: {integrity: sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==} hasBin: true + /uuid@9.0.1: + resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} + hasBin: true + dev: false + /uvu@0.5.6: resolution: {integrity: sha512-+g8ENReyr8YsOc6fv/NVJs2vFdHBnBNdfE49rshrTzDWOlUx4Gq7KOS2GD8eqhy2j+Ejq29+SbKH8yjkAqXqoA==} engines: {node: '>=8'}