diff --git a/packages/twenty-sdk/src/cli/operations/dev-once.ts b/packages/twenty-sdk/src/cli/operations/dev-once.ts index a308ac45448..58e1e2c7609 100644 --- a/packages/twenty-sdk/src/cli/operations/dev-once.ts +++ b/packages/twenty-sdk/src/cli/operations/dev-once.ts @@ -32,6 +32,9 @@ export type AppDevOnceResult = { applicationUniversalIdentifier: string; }; +const APP_DEV_UPLOAD_BATCH_SIZE = 25; +const APP_DEV_UPLOAD_BATCH_DELAY_MS = 30_000; + const innerAppDevOnce = async ( options: AppDevOnceOptions, ): Promise> => { @@ -158,27 +161,46 @@ const innerAppDevOnce = async ( }); const uploadErrors: string[] = []; + const builtFileInfos = Array.from(buildResult.builtFileInfos.values()); - const uploadPromises = Array.from(buildResult.builtFileInfos.values()).map( - async (builtFileInfo) => { - if (verbose) { - onProgress?.(`Uploading ${builtFileInfo.builtPath}`); - } + for ( + let startIndex = 0; + startIndex < builtFileInfos.length; + startIndex += APP_DEV_UPLOAD_BATCH_SIZE + ) { + const uploadBatch = builtFileInfos.slice( + startIndex, + startIndex + APP_DEV_UPLOAD_BATCH_SIZE, + ); - const result = await fileUploader.uploadFile({ - builtPath: builtFileInfo.builtPath, - fileFolder: builtFileInfo.fileFolder, - }); + await Promise.all( + uploadBatch.map(async (builtFileInfo) => { + if (verbose) { + onProgress?.(`Uploading ${builtFileInfo.builtPath}`); + } - if (!result.success) { - uploadErrors.push( - `Failed to upload ${builtFileInfo.builtPath}: ${serializeError(result.error)}`, - ); - } - }, - ); + const result = await fileUploader.uploadFile({ + builtPath: builtFileInfo.builtPath, + fileFolder: builtFileInfo.fileFolder, + }); - await Promise.all(uploadPromises); + if (!result.success) { + uploadErrors.push( + `Failed to upload ${builtFileInfo.builtPath}: ${serializeError(result.error)}`, + ); + } + }), + ); + + const hasMoreBatches = + startIndex + APP_DEV_UPLOAD_BATCH_SIZE < builtFileInfos.length; + + if (hasMoreBatches) { + await new Promise((resolve) => + setTimeout(resolve, APP_DEV_UPLOAD_BATCH_DELAY_MS), + ); + } + } if (uploadErrors.length > 0) { return { diff --git a/packages/twenty-server/src/engine/utils/__tests__/global-exception-handler.util.spec.ts b/packages/twenty-server/src/engine/utils/__tests__/global-exception-handler.util.spec.ts new file mode 100644 index 00000000000..0f9531aae67 --- /dev/null +++ b/packages/twenty-server/src/engine/utils/__tests__/global-exception-handler.util.spec.ts @@ -0,0 +1,34 @@ +import { GraphQLError } from 'graphql'; + +import { + ThrottlerException, + ThrottlerExceptionCode, +} from 'src/engine/core-modules/throttler/throttler.exception'; +import { shouldCaptureException } from 'src/engine/utils/global-exception-handler.util'; + +describe('shouldCaptureException', () => { + it('should not capture graphql errors with status code under 500', () => { + const graphQLError = new GraphQLError('Forbidden', { + extensions: { + http: { + status: 403, + }, + }, + }); + + expect(shouldCaptureException(graphQLError)).toBe(false); + }); + + it('should not capture throttler limit reached exceptions', () => { + const throttlerException = new ThrottlerException( + 'Limit reached', + ThrottlerExceptionCode.LIMIT_REACHED, + ); + + expect(shouldCaptureException(throttlerException)).toBe(false); + }); + + it('should capture unexpected errors', () => { + expect(shouldCaptureException(new Error('Unexpected'))).toBe(true); + }); +}); diff --git a/packages/twenty-server/src/engine/utils/global-exception-handler.util.ts b/packages/twenty-server/src/engine/utils/global-exception-handler.util.ts index a9500a7761c..5efbc3fa343 100644 --- a/packages/twenty-server/src/engine/utils/global-exception-handler.util.ts +++ b/packages/twenty-server/src/engine/utils/global-exception-handler.util.ts @@ -18,6 +18,10 @@ import { TimeoutError, ValidationError, } from 'src/engine/core-modules/graphql/utils/graphql-errors.util'; +import { + ThrottlerException, + ThrottlerExceptionCode, +} from 'src/engine/core-modules/throttler/throttler.exception'; import { type CustomException } from 'src/utils/custom-exception'; const graphQLPredefinedExceptions = { @@ -80,6 +84,13 @@ export const shouldCaptureException = ( return false; } + if ( + exception instanceof ThrottlerException && + exception.code === ThrottlerExceptionCode.LIMIT_REACHED + ) { + return false; + } + if (statusCode && statusCode < 500) { return false; }