Skip to content

Commit

Permalink
feat(cache-browser-local-storage): Implemented TTL support to cached …
Browse files Browse the repository at this point in the history
…items (#1457)
  • Loading branch information
SanderFlooris committed Jun 12, 2023
1 parent a5c6a64 commit 9092414
Show file tree
Hide file tree
Showing 6 changed files with 108 additions and 11 deletions.
6 changes: 3 additions & 3 deletions package.json
Expand Up @@ -98,15 +98,15 @@
"bundlesize": [
{
"path": "packages/algoliasearch/dist/algoliasearch.umd.js",
"maxSize": "8KB"
"maxSize": "8.2KB"
},
{
"path": "packages/algoliasearch/dist/algoliasearch-lite.umd.js",
"maxSize": "4.4KB"
"maxSize": "4.6KB"
},
{
"path": "packages/recommend/dist/recommend.umd.js",
"maxSize": "4.2KB"
"maxSize": "4.3KB"
}
]
}
Expand Up @@ -43,6 +43,23 @@ describe('browser local storage cache', () => {
expect(missMock.mock.calls.length).toBe(1);
});

it('reads unexpired timeToLive keys', async () => {
const cache = createBrowserLocalStorageCache({ key: version, timeToLive: 5 });
await cache.set({ key: 'foo' }, { bar: 1 });

const defaultValue = () => Promise.resolve({ bar: 2 });

const missMock = jest.fn();

expect(
await cache.get({ key: 'foo' }, defaultValue, {
miss: () => Promise.resolve(missMock()),
})
).toMatchObject({ bar: 1 });

expect(missMock.mock.calls.length).toBe(0);
});

it('deletes keys', async () => {
const cache = createBrowserLocalStorageCache({ key: version });
await cache.set({ key: 'foo' }, { bar: 1 });
Expand All @@ -62,6 +79,23 @@ describe('browser local storage cache', () => {
expect(missMock.mock.calls.length).toBe(1);
});

it('deletes expired keys', async () => {
const cache = createBrowserLocalStorageCache({ key: version, timeToLive: -1 });
await cache.set({ key: 'foo' }, { bar: 1 });

const defaultValue = () => Promise.resolve({ bar: 2 });

const missMock = jest.fn();

expect(
await cache.get({ key: 'foo' }, defaultValue, {
miss: () => Promise.resolve(missMock()),
})
).toMatchObject({ bar: 2 });

expect(missMock.mock.calls.length).toBe(1);
});

it('can be cleared', async () => {
const cache = createBrowserLocalStorageCache({ key: version });
await cache.set({ key: 'foo' }, { bar: 1 });
Expand All @@ -72,6 +106,8 @@ describe('browser local storage cache', () => {

const missMock = jest.fn();

expect(localStorage.length).toBe(0);

expect(
await cache.get({ key: 'foo' }, defaultValue, {
miss: () => Promise.resolve(missMock()),
Expand All @@ -80,7 +116,7 @@ describe('browser local storage cache', () => {

expect(missMock.mock.calls.length).toBe(1);

expect(localStorage.length).toBe(0);
expect(localStorage.getItem(`algoliasearch-client-js-${version}`)).toEqual('{}');
});

it('do throws localstorage exceptions on access', async () => {
Expand Down Expand Up @@ -139,8 +175,15 @@ describe('browser local storage cache', () => {

await cache.set(key, value);

expect(localStorage.getItem(`algoliasearch-client-js-${version}`)).toBe(
'{"{\\"foo\\":\\"bar\\"}":"foo"}'
);
const expectedValue = expect.objectContaining({
[JSON.stringify(key)]: {
timestamp: expect.any(Number),
value,
},
});

const localStorageValue = localStorage.getItem(`algoliasearch-client-js-${version}`);

expect(JSON.parse(localStorageValue ? localStorageValue : '{}')).toEqual(expectedValue);
});
});
@@ -1,6 +1,6 @@
import { Cache, CacheEvents } from '@algolia/cache-common';

import { BrowserLocalStorageOptions } from '.';
import { BrowserLocalStorageCacheItem, BrowserLocalStorageOptions } from '.';

export function createBrowserLocalStorageCache(options: BrowserLocalStorageOptions): Cache {
const namespaceKey = `algoliasearch-client-js-${options.key}`;
Expand All @@ -19,6 +19,36 @@ export function createBrowserLocalStorageCache(options: BrowserLocalStorageOptio
return JSON.parse(getStorage().getItem(namespaceKey) || '{}');
};

const setNamespace = (namespace: Record<string, any>) => {
getStorage().setItem(namespaceKey, JSON.stringify(namespace));
};

const removeOutdatedCacheItems = () => {
const timeToLive = options.timeToLive ? options.timeToLive * 1000 : null;
const namespace = getNamespace<BrowserLocalStorageCacheItem>();

const filteredNamespaceWithoutOldFormattedCacheItems = Object.fromEntries(
Object.entries(namespace).filter(([, cacheItem]) => {
return cacheItem.timestamp !== undefined;
})
);

setNamespace(filteredNamespaceWithoutOldFormattedCacheItems);

if (!timeToLive) return;

const filteredNamespaceWithoutExpiredItems = Object.fromEntries(
Object.entries(filteredNamespaceWithoutOldFormattedCacheItems).filter(([, cacheItem]) => {
const currentTimestamp = new Date().getTime();
const isExpired = cacheItem.timestamp + timeToLive < currentTimestamp;

return !isExpired;
})
);

setNamespace(filteredNamespaceWithoutExpiredItems);
};

return {
get<TValue>(
key: object | string,
Expand All @@ -29,10 +59,14 @@ export function createBrowserLocalStorageCache(options: BrowserLocalStorageOptio
): Readonly<Promise<TValue>> {
return Promise.resolve()
.then(() => {
removeOutdatedCacheItems();

const keyAsString = JSON.stringify(key);
const value = getNamespace<TValue>()[keyAsString];

return Promise.all([value || defaultValue(), value !== undefined]);
return getNamespace<Promise<BrowserLocalStorageCacheItem>>()[keyAsString];
})
.then(value => {
return Promise.all([value ? value.value : defaultValue(), value !== undefined]);
})
.then(([value, exists]) => {
return Promise.all([value, exists || events.miss(value)]);
Expand All @@ -45,7 +79,10 @@ export function createBrowserLocalStorageCache(options: BrowserLocalStorageOptio
const namespace = getNamespace();

// eslint-disable-next-line functional/immutable-data
namespace[JSON.stringify(key)] = value;
namespace[JSON.stringify(key)] = {
timestamp: new Date().getTime(),
value,
};

getStorage().setItem(namespaceKey, JSON.stringify(namespace));

Expand Down
@@ -0,0 +1,11 @@
export type BrowserLocalStorageCacheItem = {
/**
* The cache item creation timestamp.
*/
readonly timestamp: number;

/**
* The cache item value
*/
readonly value: any;
};
Expand Up @@ -4,6 +4,11 @@ export type BrowserLocalStorageOptions = {
*/
readonly key: string;

/**
* The time to live for each cached item in seconds.
*/
readonly timeToLive?: number;

/**
* The native local storage implementation.
*/
Expand Down
1 change: 1 addition & 0 deletions packages/cache-browser-local-storage/src/types/index.ts
Expand Up @@ -3,3 +3,4 @@
*/

export * from './BrowserLocalStorageOptions';
export * from './BrowserLocalStorageCacheItem';

0 comments on commit 9092414

Please sign in to comment.