mirror of https://github.com/jkjoy/sunpeiwen.git
438 lines
13 KiB
TypeScript
438 lines
13 KiB
TypeScript
import { sameValueZeroEqual } from 'fast-equals';
|
|
import microMemoize from 'micro-memoize';
|
|
import moize from '../src';
|
|
|
|
import type { Moized } from '../index.d';
|
|
|
|
const foo = 'foo';
|
|
const bar = 'bar';
|
|
const _default = 'default';
|
|
|
|
const method = jest.fn(function (one: string, two: string) {
|
|
return { one, two };
|
|
});
|
|
|
|
const methodDefaulted = jest.fn(function (one: string, two = _default) {
|
|
return { one, two };
|
|
});
|
|
|
|
const memoized = moize(method);
|
|
const memoizedDefaulted = moize(methodDefaulted);
|
|
|
|
describe('moize', () => {
|
|
afterEach(() => {
|
|
jest.clearAllMocks();
|
|
|
|
memoized.clear();
|
|
memoized.clearStats();
|
|
|
|
memoizedDefaulted.clear();
|
|
memoizedDefaulted.clearStats();
|
|
|
|
moize.collectStats(false);
|
|
});
|
|
|
|
describe('main', () => {
|
|
it('should handle a standard use-case', () => {
|
|
const result = memoized(foo, bar);
|
|
|
|
expect(result).toEqual({ one: foo, two: bar });
|
|
|
|
expect(method).toHaveBeenCalled();
|
|
|
|
method.mockClear();
|
|
|
|
let newResult;
|
|
|
|
for (let index = 0; index < 10; index++) {
|
|
newResult = memoized(foo, bar);
|
|
|
|
expect(newResult).toEqual({ one: foo, two: bar });
|
|
expect(method).not.toHaveBeenCalled();
|
|
}
|
|
});
|
|
|
|
it('should handle default parameters', () => {
|
|
const result = memoizedDefaulted(foo);
|
|
|
|
expect(result).toEqual({ one: foo, two: _default });
|
|
|
|
expect(methodDefaulted).toHaveBeenCalled();
|
|
|
|
methodDefaulted.mockClear();
|
|
|
|
let newResult;
|
|
|
|
for (let index = 0; index < 10; index++) {
|
|
newResult = memoizedDefaulted(foo);
|
|
|
|
expect(newResult).toEqual({ one: foo, two: _default });
|
|
expect(methodDefaulted).not.toHaveBeenCalled();
|
|
}
|
|
});
|
|
|
|
it('should handle a curried call of options creation', () => {
|
|
const moizer = moize({ isSerialized: true })({ maxSize: 5 })({
|
|
maxAge: 1000,
|
|
});
|
|
|
|
expect(moizer).toBeInstanceOf(Function);
|
|
|
|
const moized = moizer(jest.fn());
|
|
|
|
expect(moized.options).toEqual(
|
|
expect.objectContaining({
|
|
isSerialized: true,
|
|
maxAge: 1000,
|
|
maxSize: 5,
|
|
})
|
|
);
|
|
});
|
|
|
|
it('should handle moizing an already-moized function with additional options', () => {
|
|
const moized = moize(memoized, { maxSize: 5 });
|
|
|
|
expect(moized.originalFunction).toBe(memoized.originalFunction);
|
|
expect(moized.options).toEqual({
|
|
...memoized.options,
|
|
maxSize: 5,
|
|
});
|
|
});
|
|
|
|
it('should copy static properties from the source function', () => {
|
|
const fn = (a: any, b: any) => [a, b];
|
|
|
|
fn.foo = 'bar';
|
|
|
|
const memoized = moize(fn);
|
|
|
|
expect(memoized.foo).toBe(fn.foo);
|
|
});
|
|
});
|
|
|
|
describe('cache manipulation', () => {
|
|
it('should add an entry to cache if it does not exist', () => {
|
|
memoized(foo, bar);
|
|
|
|
const value = 'something else';
|
|
|
|
memoized.set([bar, foo], value);
|
|
|
|
expect(memoized.cacheSnapshot).toEqual({
|
|
keys: [[bar, foo]],
|
|
size: 1,
|
|
values: [value],
|
|
});
|
|
});
|
|
|
|
it('should add an entry to cache and remove the oldest one', () => {
|
|
const singleMemoized = moize(method);
|
|
|
|
singleMemoized(foo, bar);
|
|
|
|
const value = 'something else';
|
|
|
|
singleMemoized.set([bar, foo], value);
|
|
|
|
expect(singleMemoized.cacheSnapshot).toEqual({
|
|
keys: [[bar, foo]],
|
|
size: 1,
|
|
values: [value],
|
|
});
|
|
});
|
|
|
|
it('should notify of cache manipulation when adding', () => {
|
|
// eslint-disable-next-line prefer-const
|
|
let withNotifiers: Moized<typeof memoized>;
|
|
|
|
const onCacheOperation = jest.fn(function (cache, options, moized) {
|
|
expect(cache).toBe(withNotifiers.cache);
|
|
expect(options).toBe(withNotifiers.options);
|
|
expect(moized).toBe(withNotifiers);
|
|
});
|
|
|
|
withNotifiers = moize(memoized, {
|
|
onCacheAdd: onCacheOperation,
|
|
onCacheChange: onCacheOperation,
|
|
});
|
|
|
|
withNotifiers(foo, bar);
|
|
|
|
const value = 'something else';
|
|
|
|
withNotifiers.set([bar, foo], value);
|
|
|
|
expect(withNotifiers.cacheSnapshot).toEqual({
|
|
keys: [[bar, foo]],
|
|
size: 1,
|
|
values: [value],
|
|
});
|
|
|
|
expect(withNotifiers.options.onCacheAdd).toHaveBeenCalled();
|
|
expect(withNotifiers.options.onCacheChange).toHaveBeenCalled();
|
|
});
|
|
|
|
it('should update an entry to cache if it exists', () => {
|
|
memoized(foo, bar);
|
|
|
|
const value = 'something else';
|
|
|
|
memoized.set([foo, bar], value);
|
|
|
|
expect(memoized.cacheSnapshot).toEqual({
|
|
keys: [[foo, bar]],
|
|
size: 1,
|
|
values: [value],
|
|
});
|
|
});
|
|
|
|
it('should notify of cache manipulation when updating', () => {
|
|
const withNotifiers = moize(memoized, {
|
|
onCacheChange: jest.fn(function (cache, options, moized) {
|
|
expect(cache).toBe(withNotifiers.cache);
|
|
expect(options).toBe(withNotifiers.options);
|
|
expect(moized).toBe(withNotifiers);
|
|
}),
|
|
});
|
|
|
|
withNotifiers(foo, bar);
|
|
|
|
const value = 'something else';
|
|
|
|
withNotifiers.set([foo, bar], value);
|
|
|
|
expect(withNotifiers.cacheSnapshot).toEqual({
|
|
keys: [[foo, bar]],
|
|
size: 1,
|
|
values: [value],
|
|
});
|
|
|
|
expect(withNotifiers.options.onCacheChange).toHaveBeenCalled();
|
|
});
|
|
|
|
it('should get the entry in cache if it exists', () => {
|
|
const result = memoized(foo, bar);
|
|
|
|
expect(memoized.get([foo, bar])).toBe(result);
|
|
expect(memoized.get([bar, foo])).toBe(undefined);
|
|
});
|
|
|
|
it('should correctly identify the entry in cache if it exists', () => {
|
|
memoized(foo, bar);
|
|
|
|
expect(memoized.has([foo, bar])).toBe(true);
|
|
expect(memoized.has([bar, foo])).toBe(false);
|
|
});
|
|
|
|
it('should remove the entry in cache if it exists', () => {
|
|
memoized(foo, bar);
|
|
|
|
expect(memoized.has([foo, bar])).toBe(true);
|
|
|
|
const result = memoized.remove([foo, bar]);
|
|
|
|
expect(memoized.has([foo, bar])).toBe(false);
|
|
expect(result).toBe(true);
|
|
});
|
|
|
|
it('should not remove the entry in cache if does not exist', () => {
|
|
memoized(foo, bar);
|
|
|
|
expect(memoized.has([bar, foo])).toBe(false);
|
|
|
|
const result = memoized.remove([bar, foo]);
|
|
|
|
expect(memoized.has([bar, foo])).toBe(false);
|
|
expect(memoized.has([foo, bar])).toBe(true);
|
|
expect(result).toBe(false);
|
|
});
|
|
|
|
it('should notify of cache change on removal and clear the expiration', () => {
|
|
const expiringMemoized = moize(method, {
|
|
maxAge: 2000,
|
|
onCacheChange: jest.fn(),
|
|
});
|
|
|
|
expiringMemoized(foo, bar);
|
|
|
|
expect(expiringMemoized.has([foo, bar])).toBe(true);
|
|
expect(expiringMemoized.expirations.length).toBe(1);
|
|
|
|
const result = expiringMemoized.remove([foo, bar]);
|
|
|
|
expect(expiringMemoized.has([foo, bar])).toBe(false);
|
|
expect(result).toBe(true);
|
|
|
|
expect(expiringMemoized.options.onCacheChange).toHaveBeenCalledWith(
|
|
expiringMemoized.cache,
|
|
expiringMemoized.options,
|
|
expiringMemoized
|
|
);
|
|
|
|
expect(expiringMemoized.expirations.length).toBe(0);
|
|
});
|
|
|
|
it('should clear the cache', () => {
|
|
memoized(foo, bar);
|
|
|
|
expect(memoized.has([foo, bar])).toBe(true);
|
|
|
|
const result = memoized.clear();
|
|
|
|
expect(memoized.cache.size).toBe(0);
|
|
|
|
expect(memoized.has([foo, bar])).toBe(false);
|
|
expect(result).toBe(true);
|
|
});
|
|
|
|
it('should notify of the cache change on clear', () => {
|
|
const changeMemoized = moize(method, {
|
|
onCacheChange: jest.fn(),
|
|
});
|
|
|
|
changeMemoized(foo, bar);
|
|
|
|
expect(changeMemoized.has([foo, bar])).toBe(true);
|
|
|
|
const result = changeMemoized.clear();
|
|
|
|
expect(memoized.cache.size).toBe(0);
|
|
|
|
expect(changeMemoized.has([foo, bar])).toBe(false);
|
|
expect(result).toBe(true);
|
|
|
|
expect(changeMemoized.options.onCacheChange).toHaveBeenCalledWith(
|
|
changeMemoized.cache,
|
|
changeMemoized.options,
|
|
changeMemoized
|
|
);
|
|
});
|
|
|
|
it('should have the keys and values from cache', () => {
|
|
memoized(foo, bar);
|
|
|
|
const cache = memoized.cacheSnapshot;
|
|
|
|
expect(memoized.keys()).toEqual(cache.keys);
|
|
expect(memoized.values()).toEqual(cache.values);
|
|
});
|
|
|
|
it('should allow stats management of the method', () => {
|
|
moize.collectStats();
|
|
|
|
const profiled = moize(memoized, { profileName: 'profiled' });
|
|
|
|
profiled(foo, bar);
|
|
profiled(foo, bar);
|
|
profiled(foo, bar);
|
|
profiled(foo, bar);
|
|
|
|
expect(profiled.getStats()).toEqual({
|
|
calls: 4,
|
|
hits: 3,
|
|
usage: '75.0000%',
|
|
});
|
|
|
|
profiled.clearStats();
|
|
|
|
expect(profiled.getStats()).toEqual({
|
|
calls: 0,
|
|
hits: 0,
|
|
usage: '0.0000%',
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('properties', () => {
|
|
it('should have the micro-memoize options', () => {
|
|
const mmResult = microMemoize(method, { maxSize: 1 });
|
|
|
|
const { isEqual, ...options } = memoized._microMemoizeOptions;
|
|
const { isEqual: _isEqualIgnored, ...resultOptions } =
|
|
mmResult.options;
|
|
|
|
expect(options).toEqual(resultOptions);
|
|
expect(isEqual).toBe(sameValueZeroEqual);
|
|
});
|
|
|
|
it('should have cache and cacheSnapshot', () => {
|
|
memoized(foo, bar);
|
|
|
|
expect(memoized.cache).toEqual(
|
|
expect.objectContaining({
|
|
keys: [[foo, bar]],
|
|
values: [{ one: foo, two: bar }],
|
|
})
|
|
);
|
|
expect(memoized.cache.size).toBe(1);
|
|
|
|
expect(memoized.cacheSnapshot).toEqual(
|
|
expect.objectContaining({
|
|
keys: [[foo, bar]],
|
|
values: [{ one: foo, two: bar }],
|
|
})
|
|
);
|
|
expect(memoized.cacheSnapshot.size).toBe(1);
|
|
});
|
|
|
|
it('should have expirations and expirationsSnapshot', () => {
|
|
const expiringMemoized = moize(method, {
|
|
maxAge: 2000,
|
|
});
|
|
|
|
expiringMemoized(foo, bar);
|
|
|
|
expect(expiringMemoized.expirations).toEqual([
|
|
expect.objectContaining({
|
|
expirationMethod: expect.any(Function),
|
|
key: [foo, bar],
|
|
timeoutId: expect.any(Number),
|
|
}),
|
|
]);
|
|
|
|
expect(expiringMemoized.expirationsSnapshot).toEqual([
|
|
expect.objectContaining({
|
|
expirationMethod: expect.any(Function),
|
|
key: [foo, bar],
|
|
timeoutId: expect.any(Number),
|
|
}),
|
|
]);
|
|
});
|
|
|
|
it('should have the original function', () => {
|
|
expect(memoized.originalFunction).toBe(method);
|
|
});
|
|
});
|
|
|
|
describe('edge cases', () => {
|
|
it('should have a self-referring `default` property for mixed ESM/CJS environments', () => {
|
|
// @ts-ignore - `default` is not surfaced because it exists invisibly for edge-case import cross-compatibility
|
|
expect(moize.default).toBe(moize);
|
|
});
|
|
|
|
it('should prefer the `profileName` when provided', () => {
|
|
function myNamedFunction() {}
|
|
|
|
const memoized = moize(myNamedFunction, {
|
|
profileName: 'custom profile name',
|
|
});
|
|
|
|
expect(memoized.name).toBe('moized(custom profile name)');
|
|
});
|
|
|
|
it('should wrap the original function name', () => {
|
|
function myNamedFunction() {}
|
|
|
|
const memoized = moize(myNamedFunction);
|
|
|
|
expect(memoized.name).toBe('moized(myNamedFunction)');
|
|
});
|
|
|
|
it('should have an ultimate fallback for an anonymous function', () => {
|
|
const memoized = moize(() => {});
|
|
|
|
expect(memoized.name).toBe('moized(anonymous)');
|
|
});
|
|
});
|
|
});
|