mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01:00 
			
		
		
		
	feat: Make storing to local storage robust (#6139)
Handles non serializable objects like Map and Set and makes the serialisation/deserialisation process more robust generally. Closes # [UNL-322](https://linear.app/unleash/issue/UNL-322/add-types-to-local-storage-and-force-simple-types) --------- Signed-off-by: andreas-unleash <andreas@getunleash.ai>
This commit is contained in:
		
							parent
							
								
									dc977d3f40
								
							
						
					
					
						commit
						1ad5b5062a
					
				| @ -53,13 +53,26 @@ Object.defineProperty(window, 'sessionStorage', { | ||||
|     value: sessionStorageMock, | ||||
| }); | ||||
| 
 | ||||
| // Test suite
 | ||||
| describe('localStorage with TTL', () => { | ||||
|     beforeEach(() => { | ||||
|         localStorage.clear(); | ||||
|         sessionStorage.clear(); | ||||
|         vi.useFakeTimers(); | ||||
|     }); | ||||
| 
 | ||||
|     test('object should not be retrievable after TTL expires', () => { | ||||
|         const testObject = { name: 'Test', number: 123 }; | ||||
|         setLocalStorageItem('testObjectKey', testObject, 500000); | ||||
| 
 | ||||
|         vi.advanceTimersByTime(600000); | ||||
| 
 | ||||
|         const retrievedObject = getLocalStorageItem<{ | ||||
|             name: string; | ||||
|             number: number; | ||||
|         }>('testObjectKey'); | ||||
|         expect(retrievedObject).toBeUndefined(); | ||||
|     }); | ||||
| 
 | ||||
|     test('item should be retrievable before TTL expires', () => { | ||||
|         setLocalStorageItem('testKey', 'testValue', 600000); | ||||
|         expect(getLocalStorageItem('testKey')).toBe('testValue'); | ||||
| @ -82,47 +95,36 @@ describe('localStorage with TTL', () => { | ||||
|         }>('testObjectKey'); | ||||
|         expect(retrievedObject).toEqual(testObject); | ||||
|     }); | ||||
| 
 | ||||
|     test('object should not be retrievable after TTL expires', () => { | ||||
|         const testObject = { name: 'Test', number: 123 }; | ||||
|         setLocalStorageItem('testObjectKey', testObject, 500000); | ||||
| 
 | ||||
|         vi.advanceTimersByTime(600000); | ||||
| 
 | ||||
|         const retrievedObject = getLocalStorageItem<{ | ||||
|             name: string; | ||||
|             number: number; | ||||
|         }>('testObjectKey'); | ||||
|         expect(retrievedObject).toBeUndefined(); | ||||
|     test('should correctly store and retrieve a Map object', () => { | ||||
|         const testMap = new Map([ | ||||
|             ['key1', 'value1'], | ||||
|             ['key2', 'value2'], | ||||
|         ]); | ||||
|         setLocalStorageItem('testMap', testMap); | ||||
|         expect(getLocalStorageItem<Map<any, any>>('testMap')).toEqual(testMap); | ||||
|     }); | ||||
| 
 | ||||
|     test('object should be retrievable before TTL expires in sessionStorage', () => { | ||||
|         const testObject = { name: 'TestSession', number: 456 }; | ||||
|         setSessionStorageItem('testObjectKeySession', testObject, 500000); | ||||
| 
 | ||||
|         const retrievedObject = getSessionStorageItem<typeof testObject>( | ||||
|             'testObjectKeySession', | ||||
|         ); | ||||
|         expect(retrievedObject).toEqual(testObject); | ||||
|     test('should correctly store and retrieve a Set object', () => { | ||||
|         const testSet = new Set(['value1', 'value2']); | ||||
|         setLocalStorageItem('testSet', testSet); | ||||
|         expect(getLocalStorageItem<Set<any>>('testSet')).toEqual(testSet); | ||||
|     }); | ||||
| 
 | ||||
|     test('object should not be retrievable after TTL expires in sessionStorage', () => { | ||||
|         const testObject = { name: 'TestSession', number: 456 }; | ||||
|         setSessionStorageItem('testObjectKeySession', testObject, 500000); // 10 minutes TTL
 | ||||
| 
 | ||||
|         vi.advanceTimersByTime(600000); | ||||
| 
 | ||||
|         const retrievedObject = getSessionStorageItem<typeof testObject>( | ||||
|             'testObjectKeySession', | ||||
|         ); | ||||
|         expect(retrievedObject).toBeUndefined(); | ||||
|     test('should handle nested objects with arrays, Maps, and Sets', () => { | ||||
|         const complexObject = { | ||||
|             array: [1, 2, 3], | ||||
|             map: new Map([['nestedKey', 'nestedValue']]), | ||||
|             set: new Set([1, 2, 3]), | ||||
|         }; | ||||
|         setLocalStorageItem('complexObject', complexObject); | ||||
|         expect( | ||||
|             getLocalStorageItem<typeof complexObject>('complexObject'), | ||||
|         ).toEqual(complexObject); | ||||
|     }); | ||||
| 
 | ||||
|     test('should handle set with any level of nesting', () => { | ||||
|         setLocalStorageItem( | ||||
|             'testKey', | ||||
|             new Set([{ nestedSet: new Set([1, 2]) }]), | ||||
|         ); | ||||
|         expect(getLocalStorageItem('testKey')).toEqual([{ nestedSet: [1, 2] }]); | ||||
|     test('sessionStorage item should expire as per TTL', () => { | ||||
|         setSessionStorageItem('sessionTTL', 'expiring', 50); // 50ms TTL
 | ||||
|         vi.advanceTimersByTime(60); | ||||
|         expect(getSessionStorageItem('sessionTTL')).toBeUndefined(); | ||||
|     }); | ||||
| }); | ||||
|  | ||||
| @ -3,6 +3,37 @@ type Expirable<T> = { | ||||
|     expiry: number | null; | ||||
| }; | ||||
| 
 | ||||
| function isFunction(value: any): boolean { | ||||
|     return typeof value === 'function'; | ||||
| } | ||||
| 
 | ||||
| function serializer(key: string, value: any): any { | ||||
|     if (isFunction(value)) { | ||||
|         console.warn('Unable to store function.'); | ||||
|         return undefined; | ||||
|     } | ||||
|     if (value instanceof Map) { | ||||
|         return { dataType: 'Map', value: Array.from(value.entries()) }; | ||||
|     } else if (value instanceof Set) { | ||||
|         return { dataType: 'Set', value: Array.from(value) }; | ||||
|     } | ||||
|     return value; | ||||
| } | ||||
| 
 | ||||
| function deserializer(key: string, value: any): any { | ||||
|     if (value && value.dataType === 'Map') { | ||||
|         return new Map(value.value); | ||||
|     } else if (value && value.dataType === 'Set') { | ||||
|         return new Set(value.value); | ||||
|     } | ||||
|     return value; | ||||
| } | ||||
| 
 | ||||
| // Custom replacer for JSON.stringify to handle complex objects
 | ||||
| function customReplacer(key: string, value: any): any { | ||||
|     return serializer(key, value); | ||||
| } | ||||
| 
 | ||||
| // Get an item from localStorage.
 | ||||
| // Returns undefined if the browser denies access.
 | ||||
| export function getLocalStorageItem<T>(key: string): T | undefined { | ||||
| @ -39,12 +70,7 @@ export function setLocalStorageItem<T>( | ||||
|                     ? new Date().getTime() + timeToLive | ||||
|                     : null, | ||||
|         }; | ||||
|         window.localStorage.setItem( | ||||
|             key, | ||||
|             JSON.stringify(item, (_key, value) => | ||||
|                 value instanceof Set ? [...value] : value, | ||||
|             ), | ||||
|         ); | ||||
|         window.localStorage.setItem(key, JSON.stringify(item, customReplacer)); | ||||
|     } catch (err: unknown) { | ||||
|         console.warn(err); | ||||
|     } | ||||
| @ -66,9 +92,7 @@ export function setSessionStorageItem<T>( | ||||
|         }; | ||||
|         window.sessionStorage.setItem( | ||||
|             key, | ||||
|             JSON.stringify(item, (_key, value) => | ||||
|                 value instanceof Set ? [...value] : value, | ||||
|             ), | ||||
|             JSON.stringify(item, customReplacer), | ||||
|         ); | ||||
|     } catch (err: unknown) { | ||||
|         console.warn(err); | ||||
| @ -97,10 +121,14 @@ export function getSessionStorageItem<T>(key: string): T | undefined { | ||||
| 
 | ||||
| // Parse an item from localStorage.
 | ||||
| // Returns undefined if the item could not be parsed.
 | ||||
| function parseStoredItem<T>(data: string | null): T | undefined { | ||||
| function parseStoredItem<T>(data: string | null): Expirable<T> | undefined { | ||||
|     try { | ||||
|         return data ? JSON.parse(data) : undefined; | ||||
|         const item: Expirable<T> = data | ||||
|             ? JSON.parse(data, deserializer) | ||||
|             : undefined; | ||||
|         return item; | ||||
|     } catch (err: unknown) { | ||||
|         console.warn(err); | ||||
|         return undefined; | ||||
|     } | ||||
| } | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user