rework cache

This commit is contained in:
Rene
2020-12-27 00:23:55 +01:00
parent a8fa1cb6db
commit 3ec5d38ae1
6 changed files with 596 additions and 189 deletions
+2 -2
View File
@@ -5,8 +5,8 @@
"version": "0.0.1", "version": "0.0.1",
"scripts": { "scripts": {
"test": "jest --coverage --runInBand --detectOpenHandles", "test": "jest --coverage --runInBand --detectOpenHandles",
"test:jsdom": "jest --coverage --runInBand --detectOpenHandles --selectProjects jsdom", "test:jsdom": "jest --coverage --runInBand --detectOpenHandles --selectProjects jsdom --testPathPattern",
"test:pptr": "jest --coverage --runInBand --detectOpenHandles --selectProjects puppeteer", "test:pptr": "jest --coverage --runInBand --detectOpenHandles --selectProjects puppeteer --testPathPattern",
"build": "rollup -c" "build": "rollup -c"
} }
} }
@@ -1,21 +1,20 @@
import { import {
CacheUpdateInfo, CacheUpdateInfo,
CachePropsToUpdate, CachePropsToUpdate,
CacheUpdated, Cache,
OptionsWithOptionsTemplate, OptionsWithOptionsTemplate,
OptionsValidated,
transformOptions, transformOptions,
validateOptions, validateOptions,
assignDeep, assignDeep,
createCache, createCache,
isEmptyObject,
isBoolean, isBoolean,
keys,
} from 'support'; } from 'support';
import { PlainObject } from 'typings'; import { PlainObject } from 'typings';
interface LifecycleUpdateHints<O, C> { interface LifecycleUpdateHints<O, C> {
_force?: boolean; _force?: boolean;
_changedOptions?: OptionsValidated<O>; _changedOptions?: CachePropsToUpdate<O>;
_changedCache?: CachePropsToUpdate<C>; _changedCache?: CachePropsToUpdate<C>;
} }
@@ -46,7 +45,7 @@ export const createLifecycleBase = <O, C>(
defaultOptionsWithTemplate: OptionsWithOptionsTemplate<Required<O>>, defaultOptionsWithTemplate: OptionsWithOptionsTemplate<Required<O>>,
cacheUpdateInfo: CacheUpdateInfo<C>, cacheUpdateInfo: CacheUpdateInfo<C>,
initialOptions: O | undefined, initialOptions: O | undefined,
updateFunction: (changedOptions: OptionsValidated<O>, changedCache: CacheUpdated<C>) => any updateFunction: (changedOptions: Cache<O>, changedCache: Cache<C>) => any
): LifecycleBase<O, C> => { ): LifecycleBase<O, C> => {
const { _template: optionsTemplate, _options: defaultOptions } = transformOptions<Required<O>>(defaultOptionsWithTemplate); const { _template: optionsTemplate, _options: defaultOptions } = transformOptions<Required<O>>(defaultOptionsWithTemplate);
const options: Required<O> = assignDeep( const options: Required<O> = assignDeep(
@@ -55,15 +54,16 @@ export const createLifecycleBase = <O, C>(
validateOptions<O>(initialOptions || ({} as O), optionsTemplate, null, true)._validated validateOptions<O>(initialOptions || ({} as O), optionsTemplate, null, true)._validated
); );
const cacheChange = createCache<C>(cacheUpdateInfo); const cacheChange = createCache<C>(cacheUpdateInfo);
const cacheOptions = createCache<O>(options, true);
const update = (hints: LifecycleUpdateHints<O, C>) => { const update = (hints: LifecycleUpdateHints<O, C>) => {
const hasForce = isBoolean(hints._force); const hasForce = isBoolean(hints._force);
const force = hints._force === true; const force = hints._force === true;
const changedCache = cacheChange(force ? null : hints._changedCache || (hasForce ? null : []), force); const changedCache = cacheChange(force ? null : hints._changedCache || (hasForce ? null : []), force);
const changedOptions = force ? options : hints._changedOptions || ({} as O); const changedOptions = cacheOptions(force ? null : hints._changedOptions, !!hints._changedOptions || force);
if (!isEmptyObject(changedOptions) || !isEmptyObject(changedCache)) { if (changedOptions._anythingChanged || changedCache._anythingChanged) {
updateFunction(changedOptions, changedCache); updateFunction(changedOptions, changedCache);
} }
}; };
@@ -76,7 +76,7 @@ export const createLifecycleBase = <O, C>(
const { _validated: changedOptions } = validateOptions(newOptions, optionsTemplate, options, true); const { _validated: changedOptions } = validateOptions(newOptions, optionsTemplate, options, true);
assignDeep(options, changedOptions); assignDeep(options, changedOptions);
update({ _changedOptions: changedOptions }); update({ _changedOptions: keys(changedOptions) as CachePropsToUpdate<O> });
} }
return options; return options;
}, },
@@ -1,14 +1,4 @@
import { import { cssProperty, runEach, topRightBottomLeft, TRBL, equalTRBL, optionsTemplateTypes as oTypes, OptionsTemplateValue, style } from 'support';
cssProperty,
runEach,
topRightBottomLeft,
TRBL,
equalTRBL,
optionsTemplateTypes as oTypes,
OptionsTemplateValue,
style,
hasOwnProperty,
} from 'support';
import { OSTargetObject } from 'typings'; import { OSTargetObject } from 'typings';
import { createLifecycleBase, Lifecycle } from 'lifecycles/lifecycleBase'; import { createLifecycleBase, Lifecycle } from 'lifecycles/lifecycleBase';
import { getEnvironment, Environment } from 'environment'; import { getEnvironment, Environment } from 'environment';
@@ -61,9 +51,10 @@ export const createStructureLifecycle = (
}, },
initialOptions, initialOptions,
(changedOptions, changedCache) => { (changedOptions, changedCache) => {
if (hasOwnProperty(changedOptions, 'paddingAbsolute') || hasOwnProperty(changedCache, 'padding')) { const { _value: paddingAbsolute, _changed: paddingAbsoluteChanged } = changedOptions.paddingAbsolute;
const { padding } = changedCache; const { _value: padding, _changed: paddingChanged } = changedCache.padding;
const { paddingAbsolute } = changedOptions;
if (paddingAbsoluteChanged || paddingChanged) {
const paddingStyle: TRBL = { const paddingStyle: TRBL = {
t: 0, t: 0,
r: 0, r: 0,
+33 -31
View File
@@ -1,31 +1,29 @@
import { isArray, isString } from 'support/utils/types'; import { isArray, isString } from 'support/utils/types';
import { keys } from 'support/utils/object'; import { assignDeep, keys } from 'support/utils/object';
import { each } from 'support/utils/array'; import { each } from 'support/utils/array';
interface CacheEntry<T> {
_current?: T;
_previous?: T;
_changed?: boolean;
}
type Cache<T> = {
[P in keyof T]: CacheEntry<T[P]>;
};
type UpdateCacheProp<T> = <P extends keyof T>(prop: P, value: T[P], compare: EqualCachePropFunction<T, P> | null) => void; type UpdateCacheProp<T> = <P extends keyof T>(prop: P, value: T[P], compare: EqualCachePropFunction<T, P> | null) => void;
type UpdateCachePropFunction<T, P extends keyof T> = (current?: T[P], previous?: T[P]) => T[P]; type UpdateCachePropFunction<T, P extends keyof T> = (current?: T[P], previous?: T[P]) => T[P];
type EqualCachePropFunction<T, P extends keyof T> = (a?: T[P], b?: T[P]) => boolean; type EqualCachePropFunction<T, P extends keyof T> = (a?: T[P], b?: T[P]) => boolean;
export interface CacheEntry<T> {
_value?: T;
_previous?: T;
_changed: boolean;
}
export type Cache<T> = {
[P in keyof T]: CacheEntry<T[P]>;
};
export type CacheUpdated<T> = Cache<T> & { _anythingChanged: boolean };
export type CachePropsToUpdate<T> = Array<keyof T> | keyof T; export type CachePropsToUpdate<T> = Array<keyof T> | keyof T;
export type CacheUpdate<T> = (propsToUpdate?: CachePropsToUpdate<T> | null, force?: boolean) => CacheUpdated<T>; export type CacheUpdate<T> = (propsToUpdate?: CachePropsToUpdate<T> | null, force?: boolean) => CacheUpdated<T>;
export type CacheUpdated<T> = {
[P in keyof T]?: T[P];
};
export type CacheUpdateInfo<T> = { export type CacheUpdateInfo<T> = {
[P in keyof T]: UpdateCachePropFunction<T, P> | [UpdateCachePropFunction<T, P>, EqualCachePropFunction<T, P>]; [P in keyof T]: UpdateCachePropFunction<T, P> | [UpdateCachePropFunction<T, P>, EqualCachePropFunction<T, P>];
}; };
@@ -46,31 +44,34 @@ export type CacheUpdateInfo<T> = {
* If no equal function is passed a shallow comparison is carried out between the values. * If no equal function is passed a shallow comparison is carried out between the values.
* *
* @returns A function which can be called with wither one ar an array of properties which shall be updated. Optionally it can be called with the force param. * @returns A function which can be called with wither one ar an array of properties which shall be updated. Optionally it can be called with the force param.
* This function returns a object which contains all changed cache properties, if a property isn't in this object it means that it didn't change. * This function returns a object which represents the cache and its state at the time of updating (changed to previous value, current value and previous value).
*/ */
export const createCache = <T>(cacheUpdateInfo: CacheUpdateInfo<T>): CacheUpdate<T> => { export function createCache<T>(cacheUpdateInfo: CacheUpdateInfo<T>): CacheUpdate<T>;
const cache: Cache<T> = {} as T; export function createCache<T>(referenceObj: T, isReference: true): CacheUpdate<T>;
export function createCache<T>(cacheUpdateInfo: CacheUpdateInfo<T> | T, isReference?: true): CacheUpdate<T> {
const cache: Cache<T> = {} as any;
const allProps: Array<keyof T> = keys(cacheUpdateInfo) as Array<keyof T>; const allProps: Array<keyof T> = keys(cacheUpdateInfo) as Array<keyof T>;
each(allProps, (prop) => { each(allProps, (prop) => {
cache[prop] = {}; cache[prop] = { _changed: false, _value: isReference ? cacheUpdateInfo[prop] : undefined } as any;
}); });
const updateCacheProp: UpdateCacheProp<T> = (prop, value, equal): void => { const updateCacheProp: UpdateCacheProp<T> = (prop, value, equal): void => {
const curr = cache[prop]._current; const curr = cache[prop]._value;
cache[prop]._current = value; cache[prop]._value = value;
cache[prop]._previous = curr; cache[prop]._previous = curr;
cache[prop]._changed = equal ? !equal(curr, value) : curr !== value; cache[prop]._changed = equal ? !equal(curr, value) : curr !== value;
}; };
const flush = (force?: boolean): CacheUpdated<T> => { const flush = (props: Array<keyof T>, force?: boolean): CacheUpdated<T> => {
const result: CacheUpdated<T> = {} as CacheUpdated<T>; const result: CacheUpdated<T> = assignDeep({}, cache, { _anythingChanged: false });
each(allProps, (prop: keyof T) => { each(props, (prop: keyof T) => {
if (cache[prop]._changed || force) { const changed = force || cache[prop]._changed;
result[prop] = cache[prop]._current; result._anythingChanged = result._anythingChanged || changed;
}
result[prop]._changed = changed;
cache[prop]._changed = false; cache[prop]._changed = false;
}); });
@@ -83,11 +84,12 @@ export const createCache = <T>(cacheUpdateInfo: CacheUpdateInfo<T>): CacheUpdate
each(finalPropsToUpdate, (prop) => { each(finalPropsToUpdate, (prop) => {
const cacheVal = cache[prop]; const cacheVal = cache[prop];
const curr = cacheUpdateInfo[prop]; const curr = cacheUpdateInfo[prop];
const arr = isArray(curr);
const arr = isReference ? false : isArray(curr);
const value = arr ? curr[0] : curr; const value = arr ? curr[0] : curr;
const equal = arr ? curr[1] : null; const equal = arr ? curr[1] : null;
updateCacheProp(prop, value(cacheVal._current, cacheVal._previous), equal); updateCacheProp(prop, isReference ? value : value(cacheVal._value, cacheVal._previous), equal);
}); });
return flush(force); return flush(finalPropsToUpdate, force);
}; };
}; }
@@ -1,4 +1,4 @@
import { optionsTemplateTypes as oTypes } from 'support'; import { optionsTemplateTypes as oTypes, Cache } from 'support';
import { createLifecycleBase } from 'lifecycles/lifecycleBase'; import { createLifecycleBase } from 'lifecycles/lifecycleBase';
interface TestLifecycleOptions { interface TestLifecycleOptions {
@@ -37,6 +37,19 @@ const createLifecycle = (initalOptions?: TestLifecycleOptions, updateFn?: () =>
updateFn || (() => {}) updateFn || (() => {})
); );
const createOptionsUnchangedObj = (exc?: Cache<TestLifecycleOptions>) =>
expect.objectContaining({
number: exc?.number || expect.objectContaining({ _changed: false }),
string: exc?.string || expect.objectContaining({ _changed: false }),
nested: exc?.nested || expect.objectContaining({ _changed: false }),
});
const createCacheUnchangedObj = (exc?: Cache<TestLifecycleCache>) =>
expect.objectContaining({
number: exc?.number || expect.objectContaining({ _changed: false }),
constant: exc?.constant || expect.objectContaining({ _changed: false }),
object: exc?.object || expect.objectContaining({ _changed: false }),
});
describe('lifecycleBase', () => { describe('lifecycleBase', () => {
describe('options', () => { describe('options', () => {
test('correct default options', () => { test('correct default options', () => {
@@ -106,7 +119,19 @@ describe('lifecycleBase', () => {
_updateCache('number'); _updateCache('number');
expect(updateFn).toBeCalledTimes(2); expect(updateFn).toBeCalledTimes(2);
expect(updateFn).toBeCalledWith({}, { number: 2 }); expect(updateFn).toHaveBeenLastCalledWith(
expect.objectContaining({}),
expect.objectContaining({
number: {
_value: 2,
_changed: true,
_previous: 1,
},
constant: expect.objectContaining({
_changed: false,
}),
})
);
_updateCache('constant'); _updateCache('constant');
expect(updateFn).toBeCalledTimes(2); expect(updateFn).toBeCalledTimes(2);
@@ -118,11 +143,37 @@ describe('lifecycleBase', () => {
_updateCache(['number', 'object']); _updateCache(['number', 'object']);
expect(updateFn).toBeCalledTimes(2); expect(updateFn).toBeCalledTimes(2);
expect(updateFn).toBeCalledWith({}, { number: 2, object: { string: 'hihi', boolean: false } }); expect(updateFn).toHaveBeenLastCalledWith(
expect.objectContaining({}),
expect.objectContaining({
number: {
_value: 2,
_previous: 1,
_changed: true,
},
object: {
_value: { string: 'hihi', boolean: false },
_previous: { string: 'hi', boolean: true },
_changed: true,
},
})
);
_updateCache(['number', 'constant']); _updateCache(['number', 'constant']);
expect(updateFn).toBeCalledTimes(3); expect(updateFn).toBeCalledTimes(3);
expect(updateFn).toBeCalledWith({}, { number: 3 }); expect(updateFn).toHaveBeenLastCalledWith(
expect.objectContaining({}),
expect.objectContaining({
number: {
_value: 3,
_previous: 2,
_changed: true,
},
constant: expect.objectContaining({
_changed: false,
}),
})
);
_updateCache(['constant']); _updateCache(['constant']);
expect(updateFn).toBeCalledTimes(3); expect(updateFn).toBeCalledTimes(3);
@@ -135,23 +186,41 @@ describe('lifecycleBase', () => {
createLifecycle({}, updateFn); createLifecycle({}, updateFn);
expect(updateFn).toBeCalledTimes(1); expect(updateFn).toBeCalledTimes(1);
expect(updateFn).toBeCalledWith( expect(updateFn).toHaveBeenLastCalledWith(
{ expect.objectContaining({
number: 0, number: expect.objectContaining({
string: 'hi', _value: 0,
nested: { _changed: true,
boolean: false, }),
number: 0, string: expect.objectContaining({
}, _value: 'hi',
}, _changed: true,
{ }),
number: 1, nested: expect.objectContaining({
constant: false, _value: {
object: { boolean: false,
string: 'hi', number: 0,
boolean: true, },
}, _changed: true,
} }),
}),
expect.objectContaining({
number: expect.objectContaining({
_value: 1,
_changed: true,
}),
constant: expect.objectContaining({
_value: false,
_changed: true,
}),
object: expect.objectContaining({
_value: {
string: 'hi',
boolean: true,
},
_changed: true,
}),
})
); );
}); });
@@ -161,13 +230,36 @@ describe('lifecycleBase', () => {
_options({ number: 5 }); _options({ number: 5 });
expect(updateFn).toBeCalledTimes(2); expect(updateFn).toBeCalledTimes(2);
expect(updateFn).toBeCalledWith({ number: 5 }, {}); expect(updateFn).toHaveBeenLastCalledWith(
createOptionsUnchangedObj({
number: {
_value: 5,
_previous: 0,
_changed: true,
},
}),
createCacheUnchangedObj()
);
_options({ number: 5, string: 'test', nested: { number: 3 } }); _options({ number: 5, string: 'test', nested: { number: 3 } });
expect(updateFn).toBeCalledTimes(3); expect(updateFn).toBeCalledTimes(3);
expect(updateFn).toBeCalledWith({ string: 'test', nested: { number: 3 } }, {}); expect(updateFn).toHaveBeenLastCalledWith(
createOptionsUnchangedObj({
string: {
_value: 'test',
_previous: 'hi',
_changed: true,
},
nested: {
_value: expect.objectContaining({ number: 3 }),
_previous: expect.objectContaining({ number: 3 }), // because reference, number is 3 instead of expected 0
_changed: true,
},
}),
createCacheUnchangedObj()
);
_options({ number: 5, string: 'test', nested: { number: 3 } }); _options({ string: 'test', nested: { number: 3 } });
expect(updateFn).toBeCalledTimes(3); expect(updateFn).toBeCalledTimes(3);
}); });
@@ -177,11 +269,34 @@ describe('lifecycleBase', () => {
_updateCache('number'); _updateCache('number');
expect(updateFn).toBeCalledTimes(2); expect(updateFn).toBeCalledTimes(2);
expect(updateFn).toBeCalledWith({}, { number: 2 }); expect(updateFn).toHaveBeenLastCalledWith(
createOptionsUnchangedObj(),
createCacheUnchangedObj({
number: {
_value: 2,
_previous: 1,
_changed: true,
},
})
);
_updateCache(['number', 'object', 'constant']); _updateCache(['number', 'object', 'constant']);
expect(updateFn).toBeCalledTimes(3); expect(updateFn).toBeCalledTimes(3);
expect(updateFn).toBeCalledWith({}, { number: 3, object: { string: 'hihi', boolean: false } }); expect(updateFn).toHaveBeenLastCalledWith(
createOptionsUnchangedObj(),
createCacheUnchangedObj({
number: {
_value: 3,
_previous: 2,
_changed: true,
},
object: {
_value: { string: 'hihi', boolean: false },
_previous: { string: 'hi', boolean: true },
_changed: true,
},
})
);
_updateCache('constant'); _updateCache('constant');
expect(updateFn).toBeCalledTimes(3); expect(updateFn).toBeCalledTimes(3);
@@ -193,49 +308,198 @@ describe('lifecycleBase', () => {
_update(); _update();
expect(updateFn).toBeCalledTimes(2); expect(updateFn).toBeCalledTimes(2);
expect(updateFn).toBeCalledWith({}, { number: 2, object: { string: 'hihi', boolean: false } }); expect(updateFn).toHaveBeenLastCalledWith(
createOptionsUnchangedObj(),
createCacheUnchangedObj({
number: {
_value: 2,
_previous: 1,
_changed: true,
},
object: {
_value: { string: 'hihi', boolean: false },
_previous: { string: 'hi', boolean: true },
_changed: true,
},
})
);
_update(true); _update(true);
expect(updateFn).toBeCalledTimes(3); expect(updateFn).toBeCalledTimes(3);
expect(updateFn).toBeCalledWith( expect(updateFn).toHaveBeenLastCalledWith(
{ expect.objectContaining({
number: 0, number: expect.objectContaining({
string: 'hi', _value: 0,
nested: { _changed: true,
boolean: false, }),
number: 0, string: expect.objectContaining({
_value: 'hi',
_changed: true,
}),
nested: expect.objectContaining({
_value: {
boolean: false,
number: 0,
},
_changed: true,
}),
}),
expect.objectContaining({
number: {
_value: 3,
_previous: 2,
_changed: true,
},
constant: {
_value: false,
_previous: false,
_changed: true,
}, },
},
{
number: 3,
constant: false,
object: { object: {
string: 'hihihi', _value: {
boolean: true, string: 'hihihi',
boolean: true,
},
_previous: {
string: 'hihi',
boolean: false,
},
_changed: true,
}, },
} })
); );
_options({ number: 3, nested: { boolean: true } }); _options({ number: 3, nested: { boolean: true } });
_update(true); _update(true);
expect(updateFn).toBeCalledTimes(5); expect(updateFn).toBeCalledTimes(5);
expect(updateFn).toBeCalledWith( expect(updateFn).toHaveBeenLastCalledWith(
{ expect.objectContaining({
number: 3, number: expect.objectContaining({
string: 'hi', _value: 3,
nested: { _changed: true,
boolean: true, }),
number: 0, string: expect.objectContaining({
_value: 'hi',
_changed: true,
}),
nested: expect.objectContaining({
_value: {
boolean: true,
number: 0,
},
_changed: true,
}),
}),
expect.objectContaining({
number: {
_value: 4,
_previous: 3,
_changed: true,
},
constant: {
_value: false,
_previous: false,
_changed: true,
}, },
},
{
number: 4,
constant: false,
object: { object: {
string: 'hihihihi', _value: {
boolean: false, string: 'hihihihi',
boolean: false,
},
_previous: {
string: 'hihihi',
boolean: true,
},
_changed: true,
}, },
} })
);
_options({ number: 3, nested: { boolean: true } });
_update();
expect(updateFn).toBeCalledTimes(6);
expect(updateFn).toHaveBeenLastCalledWith(
createOptionsUnchangedObj({
number: expect.objectContaining({
_value: 3,
_changed: false,
}),
string: expect.objectContaining({
_value: 'hi',
_changed: false,
}),
nested: expect.objectContaining({
_value: {
boolean: true,
number: 0,
},
_changed: false,
}),
}),
createCacheUnchangedObj({
number: {
_value: 5,
_previous: 4,
_changed: true,
},
object: {
_value: { string: 'hihihihihi', boolean: true },
_previous: { string: 'hihihihi', boolean: false },
_changed: true,
},
})
);
_options({ number: 4, nested: { boolean: false }, string: 'hi' });
expect(updateFn).toBeCalledTimes(7);
expect(updateFn).toHaveBeenLastCalledWith(
createOptionsUnchangedObj({
number: expect.objectContaining({
_value: 4,
_changed: true,
}),
string: expect.objectContaining({
_value: 'hi',
_changed: false,
}),
nested: expect.objectContaining({
_value: {
boolean: false,
number: 0,
},
_changed: true,
}),
}),
createCacheUnchangedObj()
);
_update();
expect(updateFn).toBeCalledTimes(8);
expect(updateFn).toHaveBeenLastCalledWith(
createOptionsUnchangedObj({
number: expect.objectContaining({
_value: 4,
_changed: false,
}),
string: expect.objectContaining({
_value: 'hi',
_changed: false,
}),
nested: expect.objectContaining({
_value: {
boolean: false,
number: 0,
},
_changed: false,
}),
}),
createCacheUnchangedObj({
number: expect.objectContaining({
_changed: true,
}),
object: expect.objectContaining({
_changed: true,
}),
})
); );
}); });
}); });
@@ -13,7 +13,7 @@ const createUpdater = <T>(updaterReturn: (i: number) => T) => {
}; };
describe('cache', () => { describe('cache', () => {
describe('createCache', () => { describe('cache with cacheUpdateInfo object', () => {
test('creates and updates simple cache', () => { test('creates and updates simple cache', () => {
interface Test { interface Test {
number: number; number: number;
@@ -33,69 +33,81 @@ describe('cache', () => {
object: updateObj, object: updateObj,
}); });
expect(updateCache('number').number).toBe(1); expect(updateCache('number').number._value).toBe(1);
expect(updateNumberFn).toHaveBeenCalledTimes(1); expect(updateNumberFn).toHaveBeenCalledTimes(1);
expect(updateNumberFn).toHaveBeenCalledWith(undefined, undefined); expect(updateNumberFn).toHaveBeenLastCalledWith(undefined, undefined);
expect(updateCache('number').number).toBe(2); expect(updateCache('number').number._value).toBe(2);
expect(updateNumberFn).toHaveBeenCalledTimes(2); expect(updateNumberFn).toHaveBeenCalledTimes(2);
expect(updateNumberFn).toHaveBeenCalledWith(1, undefined); expect(updateNumberFn).toHaveBeenLastCalledWith(1, undefined);
expect(updateCache('number').number).toBe(3); expect(updateCache('number').number._value).toBe(3);
expect(updateNumberFn).toHaveBeenCalledTimes(3); expect(updateNumberFn).toHaveBeenCalledTimes(3);
expect(updateNumberFn).toHaveBeenCalledWith(2, 1); expect(updateNumberFn).toHaveBeenLastCalledWith(2, 1);
let { string, boolean, object, number } = updateCache('number'); let { string, boolean, object, number } = updateCache('number');
expect(string).toBe(undefined); expect(string._value).toBe(undefined);
expect(boolean).toBe(undefined); expect(string._changed).toBe(false);
expect(object).toBe(undefined); expect(boolean._value).toBe(undefined);
expect(number).toBe(4); expect(boolean._changed).toBe(false);
expect(object._value).toBe(undefined);
expect(object._changed).toBe(false);
expect(number._value).toBe(4);
expect(number._changed).toBe(true);
expect(updateBooleanFn).not.toHaveBeenCalled(); expect(updateBooleanFn).not.toHaveBeenCalled();
expect(updateStringFn).not.toHaveBeenCalled(); expect(updateStringFn).not.toHaveBeenCalled();
expect(updateObjFn).not.toHaveBeenCalled(); expect(updateObjFn).not.toHaveBeenCalled();
({ string, boolean, object, number } = updateCache(['string', 'boolean', 'object'])); ({ string, boolean, object, number } = updateCache(['string', 'boolean', 'object']));
expect(string).toBe('1'); expect(string._value).toBe('1');
expect(boolean).toBe(!!(1 % 2)); expect(string._changed).toBe(true);
expect(object).toEqual({ 1: 1 }); expect(boolean._value).toBe(!!(1 % 2));
expect(number).toBe(undefined); expect(boolean._changed).toBe(true);
expect(object._value).toEqual({ 1: 1 });
expect(object._changed).toEqual(true);
expect(number._value).toBe(4);
expect(number._changed).toBe(false);
expect(updateBooleanFn).toHaveBeenCalledTimes(1); expect(updateBooleanFn).toHaveBeenCalledTimes(1);
expect(updateBooleanFn).toHaveBeenCalledWith(undefined, undefined); expect(updateBooleanFn).toHaveBeenLastCalledWith(undefined, undefined);
expect(updateStringFn).toHaveBeenCalledTimes(1); expect(updateStringFn).toHaveBeenCalledTimes(1);
expect(updateStringFn).toHaveBeenCalledWith(undefined, undefined); expect(updateStringFn).toHaveBeenLastCalledWith(undefined, undefined);
expect(updateObjFn).toHaveBeenCalledTimes(1); expect(updateObjFn).toHaveBeenCalledTimes(1);
expect(updateObjFn).toHaveBeenCalledWith(undefined, undefined); expect(updateObjFn).toHaveBeenLastCalledWith(undefined, undefined);
updateCache(['string', 'boolean', 'object']); updateCache(['string', 'boolean', 'object']);
expect(updateBooleanFn).toHaveBeenCalledTimes(2); expect(updateBooleanFn).toHaveBeenCalledTimes(2);
expect(updateBooleanFn).toHaveBeenCalledWith(!!(1 % 2), undefined); expect(updateBooleanFn).toHaveBeenLastCalledWith(!!(1 % 2), undefined);
expect(updateStringFn).toHaveBeenCalledTimes(2); expect(updateStringFn).toHaveBeenCalledTimes(2);
expect(updateStringFn).toHaveBeenCalledWith('1', undefined); expect(updateStringFn).toHaveBeenLastCalledWith('1', undefined);
expect(updateObjFn).toHaveBeenCalledTimes(2); expect(updateObjFn).toHaveBeenCalledTimes(2);
expect(updateObjFn).toHaveBeenCalledWith({ 1: 1 }, undefined); expect(updateObjFn).toHaveBeenLastCalledWith({ 1: 1 }, undefined);
updateCache(['string', 'boolean', 'object']); updateCache(['string', 'boolean', 'object']);
expect(updateBooleanFn).toHaveBeenCalledTimes(3); expect(updateBooleanFn).toHaveBeenCalledTimes(3);
expect(updateBooleanFn).toHaveBeenCalledWith(!!(2 % 2), !!(1 % 2)); expect(updateBooleanFn).toHaveBeenLastCalledWith(!!(2 % 2), !!(1 % 2));
expect(updateStringFn).toHaveBeenCalledTimes(3); expect(updateStringFn).toHaveBeenCalledTimes(3);
expect(updateStringFn).toHaveBeenCalledWith('2', '1'); expect(updateStringFn).toHaveBeenLastCalledWith('2', '1');
expect(updateObjFn).toHaveBeenCalledTimes(3); expect(updateObjFn).toHaveBeenCalledTimes(3);
expect(updateObjFn).toHaveBeenCalledWith({ 2: 2 }, { 1: 1 }); expect(updateObjFn).toHaveBeenLastCalledWith({ 2: 2 }, { 1: 1 });
updateCache(['string', 'boolean', 'object']); updateCache(['string', 'boolean', 'object']);
({ string, boolean, object, number } = updateCache()); ({ string, boolean, object, number } = updateCache());
expect(string).toBe('5'); expect(string._value).toBe('5');
expect(boolean).toBe(!!(5 % 2)); expect(string._changed).toBe(true);
expect(object).toEqual({ 5: 5 }); expect(boolean._value).toBe(!!(5 % 2));
expect(number).toBe(5); expect(boolean._changed).toBe(true);
expect(object._value).toEqual({ 5: 5 });
expect(object._changed).toEqual(true);
expect(number._value).toBe(5);
expect(number._changed).toBe(true);
expect(updateBooleanFn).toHaveBeenCalledTimes(5); expect(updateBooleanFn).toHaveBeenCalledTimes(5);
expect(updateStringFn).toHaveBeenCalledTimes(5); expect(updateStringFn).toHaveBeenCalledTimes(5);
@@ -109,17 +121,24 @@ describe('cache', () => {
number: updateNumber, number: updateNumber,
}); });
expect(updateCache('number').number).toBe(0); let { _value, _changed } = updateCache('number').number;
expect(updateNumberFn).toHaveBeenCalledWith(undefined, undefined); expect(_value).toBe(0);
expect(_changed).toBe(true);
expect(updateNumberFn).toHaveBeenLastCalledWith(undefined, undefined);
expect(updateCache('number').number).toBe(undefined); ({ _value, _changed } = updateCache('number').number);
expect(updateNumberFn).toHaveBeenCalledWith(0, undefined); expect(_value).toBe(0);
expect(_changed).toBe(false);
expect(updateNumberFn).toHaveBeenLastCalledWith(0, undefined);
expect(updateCache('number').number).toBe(undefined); ({ _value, _changed } = updateCache('number').number);
expect(updateNumberFn).toHaveBeenCalledWith(0, 0); expect(_value).toBe(0);
expect(_changed).toBe(false);
expect(updateNumberFn).toHaveBeenLastCalledWith(0, 0);
const changed = updateCache('number'); const changed = updateCache('number');
expect(Object.prototype.hasOwnProperty.call(changed, 'changed')).toBe(false); expect(Object.prototype.hasOwnProperty.call(changed, 'changed')).toBe(false);
expect(Object.prototype.hasOwnProperty.call(changed, 'number')).toBe(true);
}); });
test('doesnt update if nothing changes with non primitives', () => { test('doesnt update if nothing changes with non primitives', () => {
@@ -136,45 +155,91 @@ describe('cache', () => {
], ],
}); });
expect(updateCache('constObj').constObj).toBe(constObj); let { _value, _changed } = updateCache('constObj').constObj;
expect(updateConstObjFn).toHaveBeenCalledWith(undefined, undefined); expect(_value).toEqual(constObj);
expect(updateCache('constObj').constObj).toBe(undefined); expect(_changed).toBe(true);
expect(updateConstObjFn).toHaveBeenCalledWith(constObj, undefined); expect(updateConstObjFn).toHaveBeenLastCalledWith(undefined, undefined);
expect(updateCache('constObj').constObj).toBe(undefined);
expect(updateConstObjFn).toHaveBeenCalledWith(constObj, constObj);
expect(Object.prototype.hasOwnProperty.call(updateCache('constObj'), 'constObj')).toBe(false);
expect(updateCache('similarObj').similarObj).toEqual(constObj); ({ _value, _changed } = updateCache('constObj').constObj);
expect(updateSimilarObjFn).toHaveBeenCalledWith(undefined, undefined); expect(_value).toEqual(constObj);
expect(updateCache('similarObj').similarObj).toEqual(constObj); expect(_changed).toBe(false);
expect(updateSimilarObjFn).toHaveBeenCalledWith(constObj, undefined); expect(updateConstObjFn).toHaveBeenLastCalledWith(constObj, undefined);
expect(updateCache('similarObj').similarObj).toEqual(constObj);
expect(updateSimilarObjFn).toHaveBeenCalledWith(constObj, constObj);
expect(Object.prototype.hasOwnProperty.call(updateCache('similarObj'), 'similarObj')).toBe(true);
expect(updateCache('comparisonObj').comparisonObj).toEqual(constObj); ({ _value, _changed } = updateCache('constObj').constObj);
expect(updateComparisonObjFn).toHaveBeenCalledWith(undefined, undefined); expect(_value).toEqual(constObj);
expect(updateCache('comparisonObj').comparisonObj).toBe(undefined); expect(_changed).toBe(false);
expect(updateComparisonObjFn).toHaveBeenCalledWith(constObj, undefined); expect(updateConstObjFn).toHaveBeenLastCalledWith(constObj, constObj);
expect(updateCache('comparisonObj').comparisonObj).toBe(undefined);
expect(updateComparisonObjFn).toHaveBeenCalledWith(constObj, constObj); ({ _value, _changed } = updateCache('similarObj').similarObj);
expect(Object.prototype.hasOwnProperty.call(updateCache('comparisonObj'), 'comparisonObj')).toBe(false); expect(_value).toEqual(constObj);
expect(_changed).toBe(true);
expect(updateSimilarObjFn).toHaveBeenLastCalledWith(undefined, undefined);
({ _value, _changed } = updateCache('similarObj').similarObj);
expect(_value).toEqual(constObj);
expect(_changed).toBe(true);
expect(updateSimilarObjFn).toHaveBeenLastCalledWith(constObj, undefined);
({ _value, _changed } = updateCache('similarObj').similarObj);
expect(_value).toEqual(constObj);
expect(_changed).toBe(true);
expect(updateSimilarObjFn).toHaveBeenLastCalledWith(constObj, constObj);
({ _value, _changed } = updateCache('comparisonObj').comparisonObj);
expect(_value).toEqual(constObj);
expect(_changed).toBe(true);
expect(updateComparisonObjFn).toHaveBeenLastCalledWith(undefined, undefined);
({ _value, _changed } = updateCache('comparisonObj').comparisonObj);
expect(_value).toEqual(constObj);
expect(_changed).toBe(false);
expect(updateComparisonObjFn).toHaveBeenLastCalledWith(constObj, undefined);
({ _value, _changed } = updateCache('comparisonObj').comparisonObj);
expect(_value).toEqual(constObj);
expect(_changed).toBe(false);
expect(updateComparisonObjFn).toHaveBeenLastCalledWith(constObj, constObj);
const result = updateCache();
expect(Object.prototype.hasOwnProperty.call(result, 'constObj')).toBe(true);
expect(Object.prototype.hasOwnProperty.call(result, 'similarObj')).toBe(true);
expect(Object.prototype.hasOwnProperty.call(result, 'comparisonObj')).toBe(true);
}); });
test('updates definitely with force', () => { test('updates definitely with force', () => {
const [updateNumberFn, updateNumber] = createUpdater<number>(() => 0); const [updateNumberFn, updateNumber] = createUpdater<number>(() => 0);
const [, updateString] = createUpdater<number>(() => 0);
const updateCache = createCache({ const updateCache = createCache({
number: updateNumber, number: updateNumber,
string: updateString,
}); });
expect(updateCache('number', true).number).toBe(0); let { _value, _changed } = updateCache('number', true).number;
expect(updateNumberFn).toHaveBeenCalledWith(undefined, undefined); expect(_value).toBe(0);
expect(_changed).toBe(true);
expect(updateNumberFn).toHaveBeenLastCalledWith(undefined, undefined);
expect(updateCache('number', true).number).toBe(0); ({ _value, _changed } = updateCache('number', true).number);
expect(updateNumberFn).toHaveBeenCalledWith(0, undefined); expect(_value).toBe(0);
expect(_changed).toBe(true);
expect(updateNumberFn).toHaveBeenLastCalledWith(0, undefined);
expect(updateCache('number', true).number).toBe(0); ({ _value, _changed } = updateCache('number', true).number);
expect(updateNumberFn).toHaveBeenCalledWith(0, 0); expect(_value).toBe(0);
expect(_changed).toBe(true);
expect(updateNumberFn).toHaveBeenLastCalledWith(0, 0);
let { number, string } = updateCache('number', true);
expect(number._changed).toBe(true);
expect(string._changed).toBe(false);
({ number, string } = updateCache(['number', 'string'], true));
expect(number._changed).toBe(true);
expect(string._changed).toBe(true);
({ number, string } = updateCache('string', true));
expect(number._changed).toBe(false);
expect(string._changed).toBe(true);
}); });
test('custom comparison on primitves', () => { test('custom comparison on primitves', () => {
@@ -185,21 +250,35 @@ describe('cache', () => {
number: [updateNumber, () => true], number: [updateNumber, () => true],
}); });
expect(updateCache('string').string).toBe('hi'); let { _value, _changed } = updateCache('string').string;
expect(updateStringFn).toHaveBeenCalledWith(undefined, undefined); expect(_value).toBe('hi');
expect(updateCache('string').string).toBe('hi'); expect(_changed).toBe(true);
expect(updateStringFn).toHaveBeenCalledWith('hi', undefined); expect(updateStringFn).toHaveBeenLastCalledWith(undefined, undefined);
expect(updateCache('string').string).toBe('hi');
expect(updateStringFn).toHaveBeenCalledWith('hi', 'hi');
expect(Object.prototype.hasOwnProperty.call(updateCache('string'), 'string')).toBe(true);
expect(updateCache('number').number).toBe(undefined); ({ _value, _changed } = updateCache('string').string);
expect(updateNumberFn).toHaveBeenCalledWith(undefined, undefined); expect(_value).toBe('hi');
expect(updateCache('number').number).toBe(undefined); expect(_changed).toBe(true);
expect(updateNumberFn).toHaveBeenCalledWith(1, undefined); expect(updateStringFn).toHaveBeenLastCalledWith('hi', undefined);
expect(updateCache('number').number).toBe(undefined);
expect(updateNumberFn).toHaveBeenCalledWith(2, 1); ({ _value, _changed } = updateCache('string').string);
expect(Object.prototype.hasOwnProperty.call(updateCache('number'), 'number')).toBe(false); expect(_value).toBe('hi');
expect(_changed).toBe(true);
expect(updateStringFn).toHaveBeenLastCalledWith('hi', 'hi');
({ _value, _changed } = updateCache('number').number);
expect(_value).toBe(1);
expect(_changed).toBe(false);
expect(updateNumberFn).toHaveBeenLastCalledWith(undefined, undefined);
({ _value, _changed } = updateCache('number').number);
expect(_value).toBe(2);
expect(_changed).toBe(false);
expect(updateNumberFn).toHaveBeenLastCalledWith(1, undefined);
({ _value, _changed } = updateCache('number').number);
expect(_value).toBe(3);
expect(_changed).toBe(false);
expect(updateNumberFn).toHaveBeenLastCalledWith(2, 1);
}); });
test('updates all entries with null or undefined as argument', () => { test('updates all entries with null or undefined as argument', () => {
@@ -211,12 +290,83 @@ describe('cache', () => {
}); });
updateCache(); updateCache();
expect(updateNumberFn).toHaveBeenCalledWith(undefined, undefined); expect(updateNumberFn).toHaveBeenLastCalledWith(undefined, undefined);
expect(updateNumberFn2).toHaveBeenCalledWith(undefined, undefined); expect(updateNumberFn2).toHaveBeenLastCalledWith(undefined, undefined);
updateCache(null); updateCache(null);
expect(updateNumberFn).toHaveBeenCalledWith(1, undefined); expect(updateNumberFn).toHaveBeenLastCalledWith(1, undefined);
expect(updateNumberFn2).toHaveBeenCalledWith(1, undefined); expect(updateNumberFn2).toHaveBeenLastCalledWith(1, undefined);
});
});
describe('cache with reference object', () => {
test('creates and updates simple cache', () => {
interface Test {
number: number;
boolean: boolean;
string: string;
object: {};
}
const refObj: Test = {
number: 0,
boolean: false,
string: 'hi',
object: {},
};
const updateCache = createCache<Test>(refObj, true);
let { _value, _changed, _previous } = updateCache('number').number;
expect(_value).toBe(0);
expect(_changed).toBe(false);
refObj.number = 1;
({ _value, _changed } = updateCache('number').number);
expect(_value).toBe(1);
expect(_changed).toBe(true);
refObj.number = 2;
({ _value, _changed } = updateCache('string').number);
expect(_value).toBe(1);
expect(_changed).toBe(false);
refObj.number = 3;
({ _value, _changed, _previous } = updateCache('number').number);
expect(_value).toBe(3);
expect(_previous).toBe(1);
expect(_changed).toBe(true);
let { number, boolean, string, object } = updateCache();
expect(number._value).toBe(3);
expect(number._changed).toBe(false);
expect(boolean._value).toBe(false);
expect(boolean._changed).toBe(false);
expect(string._value).toBe('hi');
expect(string._changed).toBe(false);
expect(object._value).toEqual({});
expect(object._changed).toBe(false);
refObj.string = 'hi2';
refObj.boolean = true;
({ number, boolean, string, object } = updateCache());
expect(number._value).toBe(3);
expect(number._changed).toBe(false);
expect(boolean._value).toBe(true);
expect(boolean._changed).toBe(true);
expect(string._value).toBe('hi2');
expect(string._changed).toBe(true);
expect(object._value).toEqual({});
expect(object._changed).toBe(false);
({ number, boolean, string, object } = updateCache(null, true));
expect(number._value).toBe(3);
expect(number._changed).toBe(true);
expect(boolean._value).toBe(true);
expect(boolean._changed).toBe(true);
expect(string._value).toBe('hi2');
expect(string._changed).toBe(true);
expect(object._value).toEqual({});
expect(object._changed).toBe(true);
}); });
}); });
}); });