Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
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"
}
]
}
Original file line number Diff line number Diff line change
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);
});
});
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export type BrowserLocalStorageCacheItem = {
/**
* The cache item creation timestamp.
*/
readonly timestamp: number;

/**
* The cache item value
*/
readonly value: any;
};
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@
*/

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