From 96c5728ed09427df38ab12459ca38944b53a271f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20Malfait?= Date: Sun, 22 Mar 2026 13:54:44 +0100 Subject: [PATCH] fix: prevent localStorage bloat from derived fields on mock metadata (#18809) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - On unauthenticated pages (login), mocked object metadata was being hydrated into the store with `readableFields` and `updatableFields` attached. These derived arrays duplicate every field per object, inflating `objectMetadataItems` in localStorage from ~70 KB to ~2 MB. Combined with other metadata keys, this exceeded Safari's 5 MB quota and caused `QuotaExceededError`, preventing real data from replacing mock data on login. - Extracted flat mock data into a dedicated file (`generatedMockObjectMetadataItemsWithRelated.ts`) and use it for store hydration, keeping the enriched version (`generatedMockObjectMetadataItems`) only for tests. - Completed `splitObjectMetadataItemWithRelated`'s contract by stripping `readableFields` and `updatableFields` at runtime (not just via TypeScript's `Omit`), matching the `FlatObjectMetadataItem` return type that omits all four relational properties. ## Root cause 1. `MinimalMetadataLoadEffect` loads mocked metadata on unauthenticated pages. 2. `generatedMockObjectMetadataItems` was produced by `enrichObjectMetadataItemsWithPermissions`, which attaches `readableFields` and `updatableFields` (full copies of the `fields` array). 3. `splitObjectMetadataItemWithRelated` only destructured `fields` and `indexMetadatas` — `readableFields`/`updatableFields` leaked into `...objectProperties` at runtime because `Omit` is a compile-time-only guard. 4. These bloated objects were written to localStorage, consuming ~2 MB instead of ~70 KB. 5. On login, the intermediate state (old composite mock `current` + new flat real `draft`) exceeded the 5 MB quota, causing a `QuotaExceededError` deadlock. ## Test plan - [ ] Open the app in a fresh Safari private window (empty localStorage) - [ ] Verify the login page loads without errors - [ ] Log in and verify metadata loads correctly - [ ] Check `localStorage` size — `metadataStoreState__objectMetadataItems` should be ~70 KB, not ~2 MB - [ ] Run existing tests: `npx nx test twenty-front` — no regressions from the mock data split Made with [Cursor](https://cursor.com) --------- Co-authored-by: Charles Bochet --- .../hooks/useLoadMockedMinimalMetadata.ts | 8 +++++--- .../testing/utils/generatedMockObjectMetadataItems.ts | 10 ++-------- .../generatedMockObjectMetadataItemsWithRelated.ts | 9 +++++++++ 3 files changed, 16 insertions(+), 11 deletions(-) create mode 100644 packages/twenty-front/src/testing/utils/generatedMockObjectMetadataItemsWithRelated.ts diff --git a/packages/twenty-front/src/modules/metadata-store/hooks/useLoadMockedMinimalMetadata.ts b/packages/twenty-front/src/modules/metadata-store/hooks/useLoadMockedMinimalMetadata.ts index d3cc2468a6..f44562bedc 100644 --- a/packages/twenty-front/src/modules/metadata-store/hooks/useLoadMockedMinimalMetadata.ts +++ b/packages/twenty-front/src/modules/metadata-store/hooks/useLoadMockedMinimalMetadata.ts @@ -11,11 +11,11 @@ export const useLoadMockedMinimalMetadata = () => { const loadMockedMinimalMetadata = useCallback(async () => { resetMetadataStore(); const [ - { generatedMockObjectMetadataItems }, + { generatedMockObjectMetadataItemsWithRelated }, { mockedViews }, { mockedNavigationMenuItems }, ] = await Promise.all([ - import('~/testing/utils/generatedMockObjectMetadataItems'), + import('~/testing/utils/generatedMockObjectMetadataItemsWithRelated'), import('~/testing/mock-data/generated/metadata/views/mock-views-data'), import( '~/testing/mock-data/generated/metadata/navigation-menu-items/mock-navigation-menu-items-data' @@ -23,7 +23,9 @@ export const useLoadMockedMinimalMetadata = () => { ]); const { flatObjects, flatFields, flatIndexes } = - splitObjectMetadataItemWithRelated(generatedMockObjectMetadataItems); + splitObjectMetadataItemWithRelated( + generatedMockObjectMetadataItemsWithRelated, + ); replaceDraft('objectMetadataItems', flatObjects, MOCKED_COLLECTION_HASH); replaceDraft('fieldMetadataItems', flatFields, MOCKED_COLLECTION_HASH); diff --git a/packages/twenty-front/src/testing/utils/generatedMockObjectMetadataItems.ts b/packages/twenty-front/src/testing/utils/generatedMockObjectMetadataItems.ts index 9ca056e6ef..2ce3855cee 100644 --- a/packages/twenty-front/src/testing/utils/generatedMockObjectMetadataItems.ts +++ b/packages/twenty-front/src/testing/utils/generatedMockObjectMetadataItems.ts @@ -1,15 +1,9 @@ import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { enrichObjectMetadataItemsWithPermissions } from '@/object-metadata/utils/enrichObjectMetadataItemsWithPermissions'; -import { mapPaginatedObjectMetadataItemsToObjectMetadataItems } from '@/object-metadata/utils/mapPaginatedObjectMetadataItemsToObjectMetadataItems'; -import { type ObjectMetadataItemsQuery } from '~/generated-metadata/graphql'; - -import { mockedStandardObjectMetadataQueryResult } from '~/testing/mock-data/generated/metadata/objects/mock-objects-metadata'; +import { generatedMockObjectMetadataItemsWithRelated } from '~/testing/utils/generatedMockObjectMetadataItemsWithRelated'; export const generatedMockObjectMetadataItems: ObjectMetadataItem[] = enrichObjectMetadataItemsWithPermissions({ - objectMetadataItems: mapPaginatedObjectMetadataItemsToObjectMetadataItems({ - pagedObjectMetadataItems: - mockedStandardObjectMetadataQueryResult as unknown as ObjectMetadataItemsQuery, - }), + objectMetadataItems: generatedMockObjectMetadataItemsWithRelated, objectPermissionsByObjectMetadataId: {}, }); diff --git a/packages/twenty-front/src/testing/utils/generatedMockObjectMetadataItemsWithRelated.ts b/packages/twenty-front/src/testing/utils/generatedMockObjectMetadataItemsWithRelated.ts new file mode 100644 index 0000000000..ae5ec08183 --- /dev/null +++ b/packages/twenty-front/src/testing/utils/generatedMockObjectMetadataItemsWithRelated.ts @@ -0,0 +1,9 @@ +import { type ObjectMetadataItemWithRelated } from '@/metadata-store/utils/splitObjectMetadataItemWithRelated'; +import { mapPaginatedObjectMetadataItemsToObjectMetadataItems } from '@/object-metadata/utils/mapPaginatedObjectMetadataItemsToObjectMetadataItems'; + +import { mockedStandardObjectMetadataQueryResult } from '~/testing/mock-data/generated/metadata/objects/mock-objects-metadata'; + +export const generatedMockObjectMetadataItemsWithRelated: ObjectMetadataItemWithRelated[] = + mapPaginatedObjectMetadataItemsToObjectMetadataItems({ + pagedObjectMetadataItems: mockedStandardObjectMetadataQueryResult, + });