You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

759 lines
19 KiB

import { Cache } from '../src/Cache';
import { MicroMemoize } from '../src/types';
import { isSameValueZero } from '../src/utils';
describe('create cache', () => {
it('should create a new cache instance with correct defaults', () => {
// @ts-ignore
const options: MicroMemoize.NormalizedOptions = {};
const cache = new Cache(options);
expect(cache.keys).toEqual([]);
expect(cache.values).toEqual([]);
expect(cache.getKeyIndex).toEqual(cache._getKeyIndexForSingle);
expect(cache.canTransformKey).toBe(false);
expect(cache.shouldCloneArguments).toBe(false);
expect(cache.shouldUpdateOnAdd).toBe(false);
expect(cache.shouldUpdateOnChange).toBe(false);
expect(cache.shouldUpdateOnHit).toBe(false);
});
it('should create a new cache instance with correct values when not matching key', () => {
// @ts-ignore
const options: MicroMemoize.NormalizedOptions = {
maxSize: 5,
transformKey(): any[] {
return [];
},
onCacheAdd() {},
onCacheChange() {},
onCacheHit() {},
};
const cache = new Cache(options);
expect(cache.keys).toEqual([]);
expect(cache.values).toEqual([]);
expect(cache.getKeyIndex).toEqual(cache._getKeyIndexForMany);
expect(cache.canTransformKey).toBe(true);
expect(cache.shouldCloneArguments).toBe(true);
expect(cache.shouldUpdateOnAdd).toBe(true);
expect(cache.shouldUpdateOnChange).toBe(true);
expect(cache.shouldUpdateOnHit).toBe(true);
});
it('should create a new cache instance with correct values when matching key', () => {
// @ts-ignore
const options: MicroMemoize.NormalizedOptions = {
isMatchingKey() {
return true;
},
};
const cache = new Cache(options);
expect(cache.keys).toEqual([]);
expect(cache.values).toEqual([]);
expect(cache.getKeyIndex).toEqual(cache._getKeyIndexFromMatchingKey);
expect(cache.canTransformKey).toBe(false);
expect(cache.shouldCloneArguments).toBe(true);
expect(cache.shouldUpdateOnAdd).toBe(false);
expect(cache.shouldUpdateOnChange).toBe(false);
expect(cache.shouldUpdateOnHit).toBe(false);
});
});
describe('cache methods', () => {
describe('getKeyIndex', () => {
it('will return -1 if no keys exist', () => {
const isEqual = (o1: any, o2: any) => o1 === o2;
// @ts-ignore
const options: MicroMemoize.NormalizedOptions = { isEqual };
const cache = new Cache(options);
const keyToMatch = ['key'];
const result = cache.getKeyIndex(keyToMatch);
expect(result).toEqual(-1);
});
it('will return the index of the match found', () => {
const isEqual = (o1: any, o2: any) => o1 === o2;
// @ts-ignore
const options: MicroMemoize.NormalizedOptions = { isEqual };
const cache = new Cache(options);
cache.keys = [['key']];
const keyToMatch = ['key'];
const result = cache.getKeyIndex(keyToMatch);
expect(result).toEqual(0);
});
it('will return the index of the match found with a larger key', () => {
const isEqual = (o1: any, o2: any) => o1 === o2;
// @ts-ignore
const options: MicroMemoize.NormalizedOptions = { isEqual };
const cache = new Cache(options);
cache.keys = [['key1', 'key2']];
const keyToMatch = ['key1', 'key2'];
const result = cache.getKeyIndex(keyToMatch);
expect(result).toEqual(0);
});
it('will return -1 if the key length is different', () => {
const isEqual = (o1: any, o2: any) => o1 === o2;
// @ts-ignore
const options: MicroMemoize.NormalizedOptions = { isEqual };
const cache = new Cache(options);
cache.keys = [['key']];
const keyToMatch = ['some', 'other key'];
const result = cache.getKeyIndex(keyToMatch);
expect(result).toEqual(-1);
});
it('will return -1 if no match found', () => {
const isEqual = (o1: any, o2: any) => o1 === o2;
// @ts-ignore
const options: MicroMemoize.NormalizedOptions = { isEqual };
const cache = new Cache(options);
cache.keys = [['key']];
const keyToMatch = ['other key'];
const result = cache.getKeyIndex(keyToMatch);
expect(result).toEqual(-1);
});
it('will return -1 if no match found with a larger key', () => {
const isEqual = (o1: any, o2: any) => o1 === o2;
// @ts-ignore
const options: MicroMemoize.NormalizedOptions = { isEqual };
const cache = new Cache(options);
cache.keys = [['key1', 'key2']];
const keyToMatch = ['keyA', 'keyB'];
const result = cache.getKeyIndex(keyToMatch);
expect(result).toEqual(-1);
});
it('will return -1 if no keys exist with larger maxSize', () => {
const isEqual = (o1: any, o2: any) => o1 === o2;
// @ts-ignore
const options: MicroMemoize.NormalizedOptions = { isEqual, maxSize: 2 };
const cache = new Cache(options);
const keyToMatch = ['key'];
const result = cache.getKeyIndex(keyToMatch);
expect(result).toEqual(-1);
});
it('will return the index of the match found with larger maxSize', () => {
const isEqual = (o1: any, o2: any) => o1 === o2;
// @ts-ignore
const options: MicroMemoize.NormalizedOptions = { isEqual, maxSize: 2 };
const cache = new Cache(options);
cache.keys = [['key'], ['other key']];
const keyToMatch = ['other key'];
const result = cache.getKeyIndex(keyToMatch);
expect(result).toEqual(1);
});
it('will return -1 if the key length is different with larger maxSize', () => {
const isEqual = (o1: any, o2: any) => o1 === o2;
// @ts-ignore
const options: MicroMemoize.NormalizedOptions = { isEqual, maxSize: 2 };
const cache = new Cache(options);
cache.keys = [['key'], ['not other key']];
const keyToMatch = ['some', 'other key'];
const result = cache.getKeyIndex(keyToMatch);
expect(result).toEqual(-1);
});
it('will return -1 if no match found with larger maxSize', () => {
const isEqual = (o1: any, o2: any) => o1 === o2;
// @ts-ignore
const options: MicroMemoize.NormalizedOptions = { isEqual, maxSize: 2 };
const cache = new Cache(options);
cache.keys = [['key'], ['other key']];
const keyToMatch = ['not present key'];
const result = cache.getKeyIndex(keyToMatch);
expect(result).toEqual(-1);
});
it('will use the isMatchingKey method is passed', () => {
const isEqual = (o1: any, o2: any) => o1 === o2;
const isMatchingKey = (o1: any, o2: any) => {
const existingKey = o1[0];
const key = o2[0];
return (
existingKey.hasOwnProperty('foo') &&
key.hasOwnProperty('foo') &&
(existingKey.bar === 'bar' || key.bar === 'baz')
);
};
// @ts-ignore
const options: MicroMemoize.NormalizedOptions = {
isEqual,
isMatchingKey,
};
const cache = new Cache(options);
cache.keys = [
[
{
bar: 'bar',
foo: 'foo',
},
],
];
const keyToMatch = [
{
bar: 'baz',
foo: 'bar',
},
];
const result = cache.getKeyIndex(keyToMatch);
expect(result).toEqual(0);
});
it('will use the isMatchingKey method is passed and maxSize is greater than 1', () => {
const isEqual = (o1: any, o2: any) => o1 === o2;
const isMatchingKey = (o1: any, o2: any) => {
const existingKey = o1[0];
const key = o2[0];
return (
existingKey.hasOwnProperty('foo') &&
key.hasOwnProperty('foo') &&
(existingKey.bar === 'bar' || key.bar === 'baz')
);
};
// @ts-ignore
const options: MicroMemoize.NormalizedOptions = {
isEqual,
isMatchingKey,
maxSize: 2,
};
const cache = new Cache(options);
cache.keys = [
[
{
bar: 'baz',
baz: 'quz',
},
],
[
{
bar: 'bar',
foo: 'foo',
},
],
];
const keyToMatch = [
{
bar: 'baz',
foo: 'bar',
},
];
const result = cache.getKeyIndex(keyToMatch);
expect(result).toEqual(1);
});
it('will return -1 if the isMatchingKey method is passed and there are no keys', () => {
const isEqual = (o1: any, o2: any) => o1 === o2;
const isMatchingKey = (o1: any, o2: any) => {
const existingKey = o1[0];
const key = o2[0];
return (
existingKey.hasOwnProperty('foo') &&
key.hasOwnProperty('foo') &&
(existingKey.bar === 'bar' || key.bar === 'baz')
);
};
// @ts-ignore
const options: MicroMemoize.NormalizedOptions = {
isEqual,
isMatchingKey,
};
const cache = new Cache(options);
const keyToMatch = ['key'];
const result = cache.getKeyIndex(keyToMatch);
expect(result).toEqual(-1);
});
it('will return -1 if the isMatchingKey method is passed and no match is found', () => {
const isEqual = (o1: any, o2: any) => o1 === o2;
const isMatchingKey = (o1: any, o2: any) => {
const existingKey = o1[0];
const key = o2[0];
return (
existingKey.hasOwnProperty('foo') &&
key.hasOwnProperty('foo') &&
(existingKey.bar === 'bar' || key.bar === 'baz')
);
};
// @ts-ignore
const options: MicroMemoize.NormalizedOptions = {
isEqual,
isMatchingKey,
};
const cache = new Cache(options);
cache.keys = [
[
{
bar: 'baz',
baz: 'quz',
},
],
];
const keyToMatch = [
{
bar: 'baz',
foo: 'bar',
},
];
const result = cache.getKeyIndex(keyToMatch);
expect(result).toEqual(-1);
});
it('will return -1 if the isMatchingKey method is passed and no match is found with a larger maxSize', () => {
const isEqual = (o1: any, o2: any) => o1 === o2;
const isMatchingKey = (o1: any, o2: any) => {
const existingKey = o1[0];
const key = o2[0];
return (
existingKey.hasOwnProperty('foo') &&
key.hasOwnProperty('foo') &&
(existingKey.bar === 'bar' || key.bar === 'baz')
);
};
// @ts-ignore
const options: MicroMemoize.NormalizedOptions = {
isEqual,
isMatchingKey,
maxSize: 2,
};
const cache = new Cache(options);
cache.keys = [
[
{
bar: 'baz',
baz: 'quz',
},
{
baz: 'quz',
quz: 'blah',
},
],
];
const keyToMatch = [
{
bar: 'baz',
foo: 'bar',
},
];
const result = cache.getKeyIndex(keyToMatch);
expect(result).toEqual(-1);
});
});
describe('orderByLru', () => {
it('will do nothing if the itemIndex is 0', () => {
// @ts-ignore
const options: MicroMemoize.NormalizedOptions = {
maxSize: 3,
};
const cache = new Cache(options);
cache.keys = [['first'], ['second'], ['third']];
cache.values = ['first', 'second', 'third'];
const itemIndex = 0;
const key = cache.keys[itemIndex]!;
const value = cache.values[itemIndex];
cache.orderByLru(key, value, itemIndex);
expect(cache.snapshot).toEqual({
keys: [['first'], ['second'], ['third']],
size: 3,
values: ['first', 'second', 'third'],
});
});
it('will place the itemIndex first in order when non-zero', () => {
// @ts-ignore
const options: MicroMemoize.NormalizedOptions = {
maxSize: 3,
};
const cache = new Cache(options);
cache.keys = [['first'], ['second'], ['third']];
cache.values = ['first', 'second', 'third'];
const itemIndex = 1;
const key = cache.keys[itemIndex]!;
const value = cache.values[itemIndex];
cache.orderByLru(key, value, itemIndex);
expect(cache.snapshot).toEqual({
keys: [['second'], ['first'], ['third']],
size: 3,
values: ['second', 'first', 'third'],
});
});
it('will add the new item to the array and remove the last when the itemIndex is the array length', () => {
// @ts-ignore
const options: MicroMemoize.NormalizedOptions = {
maxSize: 10,
};
const cache = new Cache(options);
cache.keys = [['first'], ['second'], ['third']];
cache.values = ['first', 'second', 'third'];
const itemIndex = cache.keys.length;
const key = ['key'];
const value = 'new';
cache.orderByLru(key, value, itemIndex);
expect(cache.snapshot).toEqual({
keys: [key, ['first'], ['second'], ['third']],
size: 4,
values: [value, 'first', 'second', 'third'],
});
});
it('will truncate the cache to the max size if too large by manual additions', () => {
// @ts-ignore
const options: MicroMemoize.NormalizedOptions = {
maxSize: 2,
};
const cache = new Cache(options);
cache.keys = [['first'], ['second'], ['third']];
cache.values = ['first', 'second', 'third'];
const itemIndex = cache.keys.length;
const key = ['key'];
const value = 'new';
cache.orderByLru(key, value, itemIndex);
expect(cache.snapshot).toEqual({
keys: [key, ['first']],
size: 2,
values: [value, 'first'],
});
});
});
describe('updateAsyncCache', () => {
it('will handle being settled', async () => {
const timeout = 200;
const fn = async () => {
await new Promise((resolve: Function) => {
setTimeout(resolve, timeout);
});
return 'resolved';
};
const key = ['foo'];
const memoized = (() => {}) as unknown as MicroMemoize.Memoized<Function>;
const value = fn();
// @ts-ignore
const options: MicroMemoize.NormalizedOptions = {
isEqual: isSameValueZero,
isPromise: true,
};
const cache = new Cache(options);
cache.keys = [key];
cache.values = [value];
cache.updateAsyncCache(memoized);
// this is just to prevent the unhandled rejection noise
cache.values[0].catch(() => {});
expect(cache.snapshot).toEqual({
keys: [key],
size: 1,
values: [value],
});
await new Promise((resolve: Function) => {
setTimeout(resolve, timeout + 50);
});
expect(cache.snapshot).toEqual({
keys: [key],
size: 1,
values: [value],
});
});
it('will fire cache callbacks if resolved', async () => {
const timeout = 200;
const fn = async () => {
await new Promise((resolve: Function) => {
setTimeout(resolve, timeout);
});
return 'resolved';
};
const key = ['foo'];
const memoized = (() => {}) as unknown as MicroMemoize.Memoized<Function>;
const value = fn();
// @ts-ignore
const options: MicroMemoize.NormalizedOptions = {
isEqual: isSameValueZero,
isPromise: true,
onCacheChange: jest.fn(),
onCacheHit: jest.fn(),
};
const cache = new Cache(options);
cache.keys = [key];
cache.values = [value];
cache.updateAsyncCache(memoized);
// this is just to prevent the unhandled rejection noise
cache.values[0].catch(() => {});
expect(cache.snapshot).toEqual({
keys: [key],
size: 1,
values: [value],
});
await new Promise((resolve: Function) => {
setTimeout(resolve, timeout + 50);
});
expect(cache.snapshot).toEqual({
keys: [key],
size: 1,
values: [value],
});
expect(options.onCacheHit).toHaveBeenCalledTimes(1);
expect(options.onCacheHit).toHaveBeenCalledWith(cache, options, memoized);
expect(options.onCacheChange).toHaveBeenCalledTimes(1);
expect(options.onCacheChange).toHaveBeenCalledWith(
cache,
options,
memoized,
);
});
it('will remove the key from cache when the promise is rejected', async () => {
const timeout = 200;
const fn = async () => {
await new Promise((resolve: Function, reject: Function) => {
setTimeout(() => reject(new Error('boom')), timeout);
});
};
const key = ['foo'];
const value = fn();
// @ts-ignore
const options: MicroMemoize.NormalizedOptions = {
isEqual: isSameValueZero,
isPromise: true,
onCacheChange: jest.fn(),
onCacheHit: jest.fn(),
};
const cache = new Cache(options);
cache.keys = [key];
cache.values = [value];
const memoized = (() => {}) as unknown as MicroMemoize.Memoized<Function>;
cache.updateAsyncCache(memoized);
const catcher = jest.fn();
cache.values[0].catch(catcher);
expect(cache.snapshot).toEqual({
keys: [key],
size: 1,
values: [value],
});
await new Promise((resolve: Function) => {
setTimeout(resolve, timeout + 50);
});
expect(catcher).toHaveBeenCalledTimes(1);
expect(cache.snapshot).toEqual({
keys: [],
size: 0,
values: [],
});
expect(options.onCacheHit).toHaveBeenCalledTimes(0);
expect(options.onCacheChange).toHaveBeenCalledTimes(0);
});
it('will not remove the key from cache when the promise is rejected but the key no longer exists', async () => {
const timeout = 200;
const fn = async () => {
await new Promise((resolve: Function, reject: Function) => {
setTimeout(() => reject(new Error('boom')), timeout);
});
};
const key = ['foo'];
const value = fn();
// @ts-ignore
const options: MicroMemoize.NormalizedOptions = {
isEqual: isSameValueZero,
isPromise: true,
onCacheChange: jest.fn(),
onCacheHit: jest.fn(),
};
const cache = new Cache(options);
cache.keys = [key];
cache.values = [value];
const memoized = (() => {}) as unknown as MicroMemoize.Memoized<Function>;
cache.updateAsyncCache(memoized);
const newValue = cache.values[0];
const catcher = jest.fn();
newValue.catch(catcher);
expect(cache.snapshot).toEqual({
keys: [key],
size: 1,
values: [value],
});
cache.keys = [['bar']];
// @ts-ignore
cache.values = [Promise.resolve('baz')];
await new Promise((resolve: Function) => {
setTimeout(resolve, timeout + 50);
});
expect(catcher).toHaveBeenCalledTimes(1);
expect(options.onCacheHit).toHaveBeenCalledTimes(0);
expect(options.onCacheChange).toHaveBeenCalledTimes(0);
});
});
});