Skip to content

Commit

Permalink
feat(javascript): add cache TTL and fix support message (#2474)
Browse files Browse the repository at this point in the history
  • Loading branch information
shortcuts committed Jan 3, 2024
1 parent 78daef2 commit 4546090
Show file tree
Hide file tree
Showing 4 changed files with 134 additions and 16 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,24 @@ 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 = (): DefaultValue => Promise.resolve({ bar: 2 });

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 });

Expand All @@ -53,19 +71,43 @@ 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 = (): DefaultValue => Promise.resolve({ bar: 2 });

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 });

await cache.clear();

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

expect(await cache.get({ key: 'foo' }, defaultValue, events)).toMatchObject(
{ bar: 2 }
);
expect(missMock.mock.calls.length).toBe(1);
expect(localStorage.length).toBe(0);

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

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

expect(localStorage.getItem(`algolia-client-js-${version}`)).toEqual('{}');
});

it('do throws localstorage exceptions on access', async () => {
Expand Down Expand Up @@ -122,13 +164,23 @@ describe('browser local storage cache', () => {
});
const key = { foo: 'bar' };
const value = 'foo';

expect(localStorage.getItem(`algolia-client-js-${version}`)).toBeNull();

await cache.set(key, value);

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

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

expect(JSON.parse(localStorageValue ? localStorageValue : '{}')).toEqual(
expectedValue
);
});
});
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import type { BrowserLocalStorageOptions, Cache, CacheEvents } from '../types';
import type {
BrowserLocalStorageCacheItem,
BrowserLocalStorageOptions,
Cache,
CacheEvents,
} from '../types';

export function createBrowserLocalStorageCache(
options: BrowserLocalStorageOptions
Expand All @@ -19,20 +24,61 @@ export function createBrowserLocalStorageCache(
return JSON.parse(getStorage().getItem(namespaceKey) || '{}');
}

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

function removeOutdatedCacheItems(): void {
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: Record<string, any> | string,
defaultValue: () => Promise<TValue>,
events: CacheEvents<TValue> = {
miss: (): Promise<void> => Promise.resolve(),
miss: () => Promise.resolve(),
}
): Promise<TValue> {
return Promise.resolve()
.then(() => {
const keyAsString = JSON.stringify(key);
const value = getNamespace<TValue>()[keyAsString];
removeOutdatedCacheItems();

return Promise.all([value || defaultValue(), value !== undefined]);
return getNamespace<Promise<BrowserLocalStorageCacheItem>>()[
JSON.stringify(key)
];
})
.then((value) => {
return Promise.all([
value ? value.value : defaultValue(),
value !== undefined,
]);
})
.then(([value, exists]) => {
return Promise.all([value, exists || events.miss(value)]);
Expand All @@ -47,7 +93,10 @@ export function createBrowserLocalStorageCache(
return Promise.resolve().then(() => {
const namespace = getNamespace();

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
Expand Up @@ -25,7 +25,7 @@ export class ErrorWithStackTrace extends AlgoliaError {
export class RetryError extends ErrorWithStackTrace {
constructor(stackTrace: StackFrame[]) {
super(
'Unreachable hosts - your application id may be incorrect. If the error persists, contact support@algolia.com.',
'Unreachable hosts - your application id may be incorrect. If the error persists, please create a ticket at https://support.algolia.com/ sharing steps we can use to reproduce the issue.',
stackTrace,
'RetryError'
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,12 +47,29 @@ export type BrowserLocalStorageOptions = {
*/
key: string;

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

/**
* The native local storage implementation.
*/
localStorage?: Storage;
};

export type BrowserLocalStorageCacheItem = {
/**
* The cache item creation timestamp.
*/
timestamp: number;

/**
* The cache item value.
*/
value: any;
};

export type FallbackableCacheOptions = {
/**
* List of caches order by priority.
Expand Down

0 comments on commit 4546090

Please sign in to comment.