mirror of
https://github.com/Unleash/unleash.git
synced 2025-04-24 01:18:01 +02: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,
|
value: sessionStorageMock,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Test suite
|
|
||||||
describe('localStorage with TTL', () => {
|
describe('localStorage with TTL', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
localStorage.clear();
|
localStorage.clear();
|
||||||
|
sessionStorage.clear();
|
||||||
vi.useFakeTimers();
|
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', () => {
|
test('item should be retrievable before TTL expires', () => {
|
||||||
setLocalStorageItem('testKey', 'testValue', 600000);
|
setLocalStorageItem('testKey', 'testValue', 600000);
|
||||||
expect(getLocalStorageItem('testKey')).toBe('testValue');
|
expect(getLocalStorageItem('testKey')).toBe('testValue');
|
||||||
@ -82,47 +95,36 @@ describe('localStorage with TTL', () => {
|
|||||||
}>('testObjectKey');
|
}>('testObjectKey');
|
||||||
expect(retrievedObject).toEqual(testObject);
|
expect(retrievedObject).toEqual(testObject);
|
||||||
});
|
});
|
||||||
|
test('should correctly store and retrieve a Map object', () => {
|
||||||
test('object should not be retrievable after TTL expires', () => {
|
const testMap = new Map([
|
||||||
const testObject = { name: 'Test', number: 123 };
|
['key1', 'value1'],
|
||||||
setLocalStorageItem('testObjectKey', testObject, 500000);
|
['key2', 'value2'],
|
||||||
|
]);
|
||||||
vi.advanceTimersByTime(600000);
|
setLocalStorageItem('testMap', testMap);
|
||||||
|
expect(getLocalStorageItem<Map<any, any>>('testMap')).toEqual(testMap);
|
||||||
const retrievedObject = getLocalStorageItem<{
|
|
||||||
name: string;
|
|
||||||
number: number;
|
|
||||||
}>('testObjectKey');
|
|
||||||
expect(retrievedObject).toBeUndefined();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('object should be retrievable before TTL expires in sessionStorage', () => {
|
test('should correctly store and retrieve a Set object', () => {
|
||||||
const testObject = { name: 'TestSession', number: 456 };
|
const testSet = new Set(['value1', 'value2']);
|
||||||
setSessionStorageItem('testObjectKeySession', testObject, 500000);
|
setLocalStorageItem('testSet', testSet);
|
||||||
|
expect(getLocalStorageItem<Set<any>>('testSet')).toEqual(testSet);
|
||||||
const retrievedObject = getSessionStorageItem<typeof testObject>(
|
|
||||||
'testObjectKeySession',
|
|
||||||
);
|
|
||||||
expect(retrievedObject).toEqual(testObject);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('object should not be retrievable after TTL expires in sessionStorage', () => {
|
test('should handle nested objects with arrays, Maps, and Sets', () => {
|
||||||
const testObject = { name: 'TestSession', number: 456 };
|
const complexObject = {
|
||||||
setSessionStorageItem('testObjectKeySession', testObject, 500000); // 10 minutes TTL
|
array: [1, 2, 3],
|
||||||
|
map: new Map([['nestedKey', 'nestedValue']]),
|
||||||
vi.advanceTimersByTime(600000);
|
set: new Set([1, 2, 3]),
|
||||||
|
};
|
||||||
const retrievedObject = getSessionStorageItem<typeof testObject>(
|
setLocalStorageItem('complexObject', complexObject);
|
||||||
'testObjectKeySession',
|
expect(
|
||||||
);
|
getLocalStorageItem<typeof complexObject>('complexObject'),
|
||||||
expect(retrievedObject).toBeUndefined();
|
).toEqual(complexObject);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should handle set with any level of nesting', () => {
|
test('sessionStorage item should expire as per TTL', () => {
|
||||||
setLocalStorageItem(
|
setSessionStorageItem('sessionTTL', 'expiring', 50); // 50ms TTL
|
||||||
'testKey',
|
vi.advanceTimersByTime(60);
|
||||||
new Set([{ nestedSet: new Set([1, 2]) }]),
|
expect(getSessionStorageItem('sessionTTL')).toBeUndefined();
|
||||||
);
|
|
||||||
expect(getLocalStorageItem('testKey')).toEqual([{ nestedSet: [1, 2] }]);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -3,6 +3,37 @@ type Expirable<T> = {
|
|||||||
expiry: number | null;
|
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.
|
// Get an item from localStorage.
|
||||||
// Returns undefined if the browser denies access.
|
// Returns undefined if the browser denies access.
|
||||||
export function getLocalStorageItem<T>(key: string): T | undefined {
|
export function getLocalStorageItem<T>(key: string): T | undefined {
|
||||||
@ -39,12 +70,7 @@ export function setLocalStorageItem<T>(
|
|||||||
? new Date().getTime() + timeToLive
|
? new Date().getTime() + timeToLive
|
||||||
: null,
|
: null,
|
||||||
};
|
};
|
||||||
window.localStorage.setItem(
|
window.localStorage.setItem(key, JSON.stringify(item, customReplacer));
|
||||||
key,
|
|
||||||
JSON.stringify(item, (_key, value) =>
|
|
||||||
value instanceof Set ? [...value] : value,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
console.warn(err);
|
console.warn(err);
|
||||||
}
|
}
|
||||||
@ -66,9 +92,7 @@ export function setSessionStorageItem<T>(
|
|||||||
};
|
};
|
||||||
window.sessionStorage.setItem(
|
window.sessionStorage.setItem(
|
||||||
key,
|
key,
|
||||||
JSON.stringify(item, (_key, value) =>
|
JSON.stringify(item, customReplacer),
|
||||||
value instanceof Set ? [...value] : value,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
console.warn(err);
|
console.warn(err);
|
||||||
@ -97,10 +121,14 @@ export function getSessionStorageItem<T>(key: string): T | undefined {
|
|||||||
|
|
||||||
// Parse an item from localStorage.
|
// Parse an item from localStorage.
|
||||||
// Returns undefined if the item could not be parsed.
|
// 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 {
|
try {
|
||||||
return data ? JSON.parse(data) : undefined;
|
const item: Expirable<T> = data
|
||||||
|
? JSON.parse(data, deserializer)
|
||||||
|
: undefined;
|
||||||
|
return item;
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
console.warn(err);
|
console.warn(err);
|
||||||
|
return undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user