diff --git a/packages/twenty-front/src/modules/activities/files/utils/__tests__/fetchCsvPreview.test.ts b/packages/twenty-front/src/modules/activities/files/utils/__tests__/fetchCsvPreview.test.ts index 5839e449b19..932a9ac4ece 100644 --- a/packages/twenty-front/src/modules/activities/files/utils/__tests__/fetchCsvPreview.test.ts +++ b/packages/twenty-front/src/modules/activities/files/utils/__tests__/fetchCsvPreview.test.ts @@ -1,9 +1,19 @@ import { fetchCsvPreview } from '@/activities/files/utils/fetchCsvPreview'; const mockFetch = (text: string) => { + const encoder = new TextEncoder(); + const encoded = encoder.encode(text); + const stream = new ReadableStream({ + start(controller) { + controller.enqueue(encoded); + controller.close(); + }, + }); + global.fetch = jest.fn(() => Promise.resolve({ text: () => Promise.resolve(text), + body: stream, } as unknown as Response), ); }; diff --git a/packages/twenty-front/src/modules/activities/files/utils/fetchCsvPreview.ts b/packages/twenty-front/src/modules/activities/files/utils/fetchCsvPreview.ts index 3ad7412e30d..96bde1f3853 100644 --- a/packages/twenty-front/src/modules/activities/files/utils/fetchCsvPreview.ts +++ b/packages/twenty-front/src/modules/activities/files/utils/fetchCsvPreview.ts @@ -2,14 +2,48 @@ import Papa from 'papaparse'; const DEFAULT_PREVIEW_ROWS = 50; +// Read only enough bytes from the response stream to extract preview rows, +// avoiding downloading the entire file (which can be many MB) and blocking +// the main thread while converting it to text via response.text(). +const MAX_PREVIEW_BYTES = 1024 * 512; // 512 KB — generous for 50 rows of CSV + export type CsvPreviewData = { headers: string[]; rows: string[][]; }; +const readPartialResponseText = async ( + response: Response, +): Promise => { + const reader = response.body?.getReader(); + + if (!reader) { + return response.text(); + } + + const decoder = new TextDecoder(); + let accumulated = ''; + + try { + while (accumulated.length < MAX_PREVIEW_BYTES) { + const { done, value } = await reader.read(); + + if (done) { + break; + } + + accumulated += decoder.decode(value, { stream: true }); + } + } finally { + reader.cancel(); + } + + return accumulated; +}; + export const fetchCsvPreview = async (url: string): Promise => { const response = await fetch(url); - const text = await response.text(); + const text = await readPartialResponseText(response); const result = Papa.parse(text, { preview: DEFAULT_PREVIEW_ROWS + 1, // +1 for header row