feat: max entries on memory storage #539

This commit is contained in:
arthurfiorette 2023-05-25 13:14:11 -03:00
parent bd6cb4ad24
commit ebd400bfa4
No known key found for this signature in database
GPG Key ID: 9D190CD53C53C555
3 changed files with 139 additions and 28 deletions

View File

@ -33,7 +33,7 @@ option to clone the response before saving it. _Just like
others._
For long running processes, you can avoid memory leaks by using playing with the
`cleanupInterval` option.
`cleanupInterval` option. And can reduce memory usage with `maxEntries`.
```ts
import Axios from 'axios';
@ -43,7 +43,8 @@ setupCache(axios, {
// You don't need to to that, as it is the default option.
storage: buildMemoryStorage(
/* cloneData default=*/ false,
/* cleanupInterval default=*/ false
/* cleanupInterval default=*/ false,
/* maxEntries default=*/ false
)
});
```

View File

@ -35,13 +35,38 @@ declare const structuredClone: (<T>(value: T) => T) | undefined;
*
* @param {number | false} cleanupInterval The interval in milliseconds to run a
* setInterval job of cleaning old entries. If false, the job will not be created. Disabled is default
*
* @param {number | false} maxEntries The maximum number of entries to keep in the storage. Its hard to
* determine the size of the entries, so a smart FIFO order is used to determine eviction. If false,
* no check will be done and you may grow up memory usage. Disabled is default
*/
export function buildMemoryStorage(
cloneData = false,
cleanupInterval: number | false = false
cleanupInterval: number | false = false,
maxEntries: number | false = false
) {
const storage = buildStorage({
set: (key, value) => {
if (maxEntries) {
let keys = Object.keys(storage.data);
// Tries to cleanup first
if (keys.length >= maxEntries) {
storage.cleanup();
// Recalculates the keys
keys = Object.keys(storage.data);
// Keeps deleting until there's space
while (keys.length >= maxEntries) {
// There's always at least one key here, otherwise it would not be
// in the loop.
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
delete storage.data[keys.shift()!];
}
}
}
storage.data[key] = value;
},
@ -70,35 +95,37 @@ export function buildMemoryStorage(
// When this program gets running for more than the specified interval, there's a good
// chance of it being a long-running process or at least have a lot of entries. Therefore,
// "faster" loop is more important than code readability.
if (cleanupInterval) {
storage.cleaner = setInterval(() => {
const keys = Object.keys(storage.data);
storage.cleanup = () => {
const keys = Object.keys(storage.data);
let i = -1,
value: StorageValue,
key: string;
let i = -1,
value: StorageValue,
key: string;
// Looping forward, as older entries are more likely to be expired
// than newer ones.
while (++i < keys.length) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
(key = keys[i]!), (value = storage.data[key]!);
// Looping forward, as older entries are more likely to be expired
// than newer ones.
while (++i < keys.length) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
(key = keys[i]!), (value = storage.data[key]!);
if (value.state === 'empty') {
// this storage returns void.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
storage.remove(key);
continue;
}
// If the value is expired and can't be stale, remove it
if (value.state === 'cached' && isExpired(value) && !canStale(value)) {
// this storage returns void.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
storage.remove(key);
}
if (value.state === 'empty') {
// this storage returns void.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
storage.remove(key);
continue;
}
}, cleanupInterval);
// If the value is expired and can't be stale, remove it
if (value.state === 'cached' && isExpired(value) && !canStale(value)) {
// this storage returns void.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
storage.remove(key);
}
}
};
if (cleanupInterval) {
storage.cleaner = setInterval(storage.cleanup, cleanupInterval);
}
return storage;
@ -108,4 +135,6 @@ export type MemoryStorage = AxiosStorage & {
data: Record<string, StorageValue>;
/** The job responsible to cleaning old entries */
cleaner: ReturnType<typeof setInterval>;
/** Tries to remove any invalid entry from the memory */
cleanup: () => void;
};

View File

@ -110,4 +110,85 @@ describe('tests memory storage', () => {
// Clears handle
clearTimeout(storage.cleaner);
});
it('tests maxEntries without cleanup', async () => {
const storage = buildMemoryStorage(false, false, 2);
await storage.set('key', {
state: 'cached',
createdAt: Date.now(),
ttl: 1000 * 60 * 5, // 5 Minutes
data: { ...EMPTY_RESPONSE, data: 'data' }
});
await storage.set('key2', {
state: 'cached',
createdAt: Date.now(),
ttl: 1000 * 60 * 5, // 5 Minutes
data: { ...EMPTY_RESPONSE, data: 'data' }
});
expect(Object.keys(storage.data)).toHaveLength(2);
expect(storage.data['key']).toBeDefined();
expect(storage.data['key2']).toBeDefined();
expect(storage.data['key3']).toBeUndefined();
await storage.set('key3', {
state: 'cached',
createdAt: Date.now(),
ttl: 1000 * 60 * 5, // 5 Minutes
data: { ...EMPTY_RESPONSE, data: 'data' }
});
expect(Object.keys(storage.data)).toHaveLength(2);
expect(storage.data['key']).toBeUndefined();
expect(storage.data['key2']).toBeDefined();
expect(storage.data['key3']).toBeDefined();
});
it('tests maxEntries with cleanup', async () => {
const storage = buildMemoryStorage(false, false, 3);
await storage.set('exp', {
state: 'cached',
createdAt: Date.now() - 1000,
ttl: 500,
data: { ...EMPTY_RESPONSE, data: 'data' }
});
await storage.set('not exp', {
state: 'cached',
createdAt: Date.now(),
ttl: 1000 * 60 * 5, // 5 Minutes
data: { ...EMPTY_RESPONSE, data: 'data' }
});
await storage.set('exp2', {
state: 'cached',
createdAt: Date.now() - 1000,
ttl: 500,
data: { ...EMPTY_RESPONSE, data: 'data' }
});
expect(Object.keys(storage.data)).toHaveLength(3);
expect(storage.data['exp']).toBeDefined();
expect(storage.data['not exp']).toBeDefined();
expect(storage.data['exp2']).toBeDefined();
expect(storage.data['key']).toBeUndefined();
await storage.set('key', {
state: 'cached',
createdAt: Date.now(),
ttl: 1000 * 60 * 5, // 5 Minutes
data: { ...EMPTY_RESPONSE, data: 'data' }
});
expect(Object.keys(storage.data)).toHaveLength(2);
expect(storage.data['exp']).toBeUndefined();
expect(storage.data['exp2']).toBeUndefined();
expect(storage.data['not exp']).toBeDefined();
expect(storage.data['key']).toBeDefined();
});
});