From 3ec5d38ae1de3c651b4187209d6f6379c81bd25b Mon Sep 17 00:00:00 2001 From: Rene Date: Sun, 27 Dec 2020 00:23:55 +0100 Subject: [PATCH] rework cache --- packages/overlayscrollbars/package.json | 4 +- .../src/lifecycles/lifecycleBase.ts | 16 +- .../src/lifecycles/structureLifecycle.ts | 19 +- .../src/support/cache/cache.ts | 64 +-- .../jsdom/lifecycles/lifecycleBase.test.ts | 374 +++++++++++++++--- .../tests/jsdom/support/cache/cache.test.ts | 308 +++++++++++---- 6 files changed, 596 insertions(+), 189 deletions(-) diff --git a/packages/overlayscrollbars/package.json b/packages/overlayscrollbars/package.json index 9203c52..f64d2e1 100644 --- a/packages/overlayscrollbars/package.json +++ b/packages/overlayscrollbars/package.json @@ -5,8 +5,8 @@ "version": "0.0.1", "scripts": { "test": "jest --coverage --runInBand --detectOpenHandles", - "test:jsdom": "jest --coverage --runInBand --detectOpenHandles --selectProjects jsdom", - "test:pptr": "jest --coverage --runInBand --detectOpenHandles --selectProjects puppeteer", + "test:jsdom": "jest --coverage --runInBand --detectOpenHandles --selectProjects jsdom --testPathPattern", + "test:pptr": "jest --coverage --runInBand --detectOpenHandles --selectProjects puppeteer --testPathPattern", "build": "rollup -c" } } diff --git a/packages/overlayscrollbars/src/lifecycles/lifecycleBase.ts b/packages/overlayscrollbars/src/lifecycles/lifecycleBase.ts index 86f6749..bda5eab 100644 --- a/packages/overlayscrollbars/src/lifecycles/lifecycleBase.ts +++ b/packages/overlayscrollbars/src/lifecycles/lifecycleBase.ts @@ -1,21 +1,20 @@ import { CacheUpdateInfo, CachePropsToUpdate, - CacheUpdated, + Cache, OptionsWithOptionsTemplate, - OptionsValidated, transformOptions, validateOptions, assignDeep, createCache, - isEmptyObject, isBoolean, + keys, } from 'support'; import { PlainObject } from 'typings'; interface LifecycleUpdateHints { _force?: boolean; - _changedOptions?: OptionsValidated; + _changedOptions?: CachePropsToUpdate; _changedCache?: CachePropsToUpdate; } @@ -46,7 +45,7 @@ export const createLifecycleBase = ( defaultOptionsWithTemplate: OptionsWithOptionsTemplate>, cacheUpdateInfo: CacheUpdateInfo, initialOptions: O | undefined, - updateFunction: (changedOptions: OptionsValidated, changedCache: CacheUpdated) => any + updateFunction: (changedOptions: Cache, changedCache: Cache) => any ): LifecycleBase => { const { _template: optionsTemplate, _options: defaultOptions } = transformOptions>(defaultOptionsWithTemplate); const options: Required = assignDeep( @@ -55,15 +54,16 @@ export const createLifecycleBase = ( validateOptions(initialOptions || ({} as O), optionsTemplate, null, true)._validated ); const cacheChange = createCache(cacheUpdateInfo); + const cacheOptions = createCache(options, true); const update = (hints: LifecycleUpdateHints) => { const hasForce = isBoolean(hints._force); const force = hints._force === true; 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); } }; @@ -76,7 +76,7 @@ export const createLifecycleBase = ( const { _validated: changedOptions } = validateOptions(newOptions, optionsTemplate, options, true); assignDeep(options, changedOptions); - update({ _changedOptions: changedOptions }); + update({ _changedOptions: keys(changedOptions) as CachePropsToUpdate }); } return options; }, diff --git a/packages/overlayscrollbars/src/lifecycles/structureLifecycle.ts b/packages/overlayscrollbars/src/lifecycles/structureLifecycle.ts index 32c9514..b086700 100644 --- a/packages/overlayscrollbars/src/lifecycles/structureLifecycle.ts +++ b/packages/overlayscrollbars/src/lifecycles/structureLifecycle.ts @@ -1,14 +1,4 @@ -import { - cssProperty, - runEach, - topRightBottomLeft, - TRBL, - equalTRBL, - optionsTemplateTypes as oTypes, - OptionsTemplateValue, - style, - hasOwnProperty, -} from 'support'; +import { cssProperty, runEach, topRightBottomLeft, TRBL, equalTRBL, optionsTemplateTypes as oTypes, OptionsTemplateValue, style } from 'support'; import { OSTargetObject } from 'typings'; import { createLifecycleBase, Lifecycle } from 'lifecycles/lifecycleBase'; import { getEnvironment, Environment } from 'environment'; @@ -61,9 +51,10 @@ export const createStructureLifecycle = ( }, initialOptions, (changedOptions, changedCache) => { - if (hasOwnProperty(changedOptions, 'paddingAbsolute') || hasOwnProperty(changedCache, 'padding')) { - const { padding } = changedCache; - const { paddingAbsolute } = changedOptions; + const { _value: paddingAbsolute, _changed: paddingAbsoluteChanged } = changedOptions.paddingAbsolute; + const { _value: padding, _changed: paddingChanged } = changedCache.padding; + + if (paddingAbsoluteChanged || paddingChanged) { const paddingStyle: TRBL = { t: 0, r: 0, diff --git a/packages/overlayscrollbars/src/support/cache/cache.ts b/packages/overlayscrollbars/src/support/cache/cache.ts index 66f9dd2..e630896 100644 --- a/packages/overlayscrollbars/src/support/cache/cache.ts +++ b/packages/overlayscrollbars/src/support/cache/cache.ts @@ -1,31 +1,29 @@ 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'; -interface CacheEntry { - _current?: T; - _previous?: T; - _changed?: boolean; -} - -type Cache = { - [P in keyof T]: CacheEntry; -}; - type UpdateCacheProp =

(prop: P, value: T[P], compare: EqualCachePropFunction | null) => void; type UpdateCachePropFunction = (current?: T[P], previous?: T[P]) => T[P]; type EqualCachePropFunction = (a?: T[P], b?: T[P]) => boolean; +export interface CacheEntry { + _value?: T; + _previous?: T; + _changed: boolean; +} + +export type Cache = { + [P in keyof T]: CacheEntry; +}; + +export type CacheUpdated = Cache & { _anythingChanged: boolean }; + export type CachePropsToUpdate = Array | keyof T; export type CacheUpdate = (propsToUpdate?: CachePropsToUpdate | null, force?: boolean) => CacheUpdated; -export type CacheUpdated = { - [P in keyof T]?: T[P]; -}; - export type CacheUpdateInfo = { [P in keyof T]: UpdateCachePropFunction | [UpdateCachePropFunction, EqualCachePropFunction]; }; @@ -46,31 +44,34 @@ export type CacheUpdateInfo = { * 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. - * 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 = (cacheUpdateInfo: CacheUpdateInfo): CacheUpdate => { - const cache: Cache = {} as T; +export function createCache(cacheUpdateInfo: CacheUpdateInfo): CacheUpdate; +export function createCache(referenceObj: T, isReference: true): CacheUpdate; +export function createCache(cacheUpdateInfo: CacheUpdateInfo | T, isReference?: true): CacheUpdate { + const cache: Cache = {} as any; const allProps: Array = keys(cacheUpdateInfo) as Array; each(allProps, (prop) => { - cache[prop] = {}; + cache[prop] = { _changed: false, _value: isReference ? cacheUpdateInfo[prop] : undefined } as any; }); const updateCacheProp: UpdateCacheProp = (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]._changed = equal ? !equal(curr, value) : curr !== value; }; - const flush = (force?: boolean): CacheUpdated => { - const result: CacheUpdated = {} as CacheUpdated; + const flush = (props: Array, force?: boolean): CacheUpdated => { + const result: CacheUpdated = assignDeep({}, cache, { _anythingChanged: false }); - each(allProps, (prop: keyof T) => { - if (cache[prop]._changed || force) { - result[prop] = cache[prop]._current; - } + each(props, (prop: keyof T) => { + const changed = force || cache[prop]._changed; + result._anythingChanged = result._anythingChanged || changed; + + result[prop]._changed = changed; cache[prop]._changed = false; }); @@ -83,11 +84,12 @@ export const createCache = (cacheUpdateInfo: CacheUpdateInfo): CacheUpdate each(finalPropsToUpdate, (prop) => { const cacheVal = cache[prop]; const curr = cacheUpdateInfo[prop]; - const arr = isArray(curr); + + const arr = isReference ? false : isArray(curr); const value = arr ? curr[0] : curr; 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); }; -}; +} diff --git a/packages/overlayscrollbars/tests/jsdom/lifecycles/lifecycleBase.test.ts b/packages/overlayscrollbars/tests/jsdom/lifecycles/lifecycleBase.test.ts index 32f7a9e..2d0f801 100644 --- a/packages/overlayscrollbars/tests/jsdom/lifecycles/lifecycleBase.test.ts +++ b/packages/overlayscrollbars/tests/jsdom/lifecycles/lifecycleBase.test.ts @@ -1,4 +1,4 @@ -import { optionsTemplateTypes as oTypes } from 'support'; +import { optionsTemplateTypes as oTypes, Cache } from 'support'; import { createLifecycleBase } from 'lifecycles/lifecycleBase'; interface TestLifecycleOptions { @@ -37,6 +37,19 @@ const createLifecycle = (initalOptions?: TestLifecycleOptions, updateFn?: () => updateFn || (() => {}) ); +const createOptionsUnchangedObj = (exc?: Cache) => + 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) => + 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('options', () => { test('correct default options', () => { @@ -106,7 +119,19 @@ describe('lifecycleBase', () => { _updateCache('number'); 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'); expect(updateFn).toBeCalledTimes(2); @@ -118,11 +143,37 @@ describe('lifecycleBase', () => { _updateCache(['number', 'object']); 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']); 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']); expect(updateFn).toBeCalledTimes(3); @@ -135,23 +186,41 @@ describe('lifecycleBase', () => { createLifecycle({}, updateFn); expect(updateFn).toBeCalledTimes(1); - expect(updateFn).toBeCalledWith( - { - number: 0, - string: 'hi', - nested: { - boolean: false, - number: 0, - }, - }, - { - number: 1, - constant: false, - object: { - string: 'hi', - boolean: true, - }, - } + expect(updateFn).toHaveBeenLastCalledWith( + expect.objectContaining({ + number: expect.objectContaining({ + _value: 0, + _changed: true, + }), + string: expect.objectContaining({ + _value: 'hi', + _changed: true, + }), + nested: expect.objectContaining({ + _value: { + boolean: false, + number: 0, + }, + _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 }); 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 } }); 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); }); @@ -177,11 +269,34 @@ describe('lifecycleBase', () => { _updateCache('number'); 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']); 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'); expect(updateFn).toBeCalledTimes(3); @@ -193,49 +308,198 @@ describe('lifecycleBase', () => { _update(); 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); expect(updateFn).toBeCalledTimes(3); - expect(updateFn).toBeCalledWith( - { - number: 0, - string: 'hi', - nested: { - boolean: false, - number: 0, + expect(updateFn).toHaveBeenLastCalledWith( + expect.objectContaining({ + number: expect.objectContaining({ + _value: 0, + _changed: true, + }), + 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: { - string: 'hihihi', - boolean: true, + _value: { + string: 'hihihi', + boolean: true, + }, + _previous: { + string: 'hihi', + boolean: false, + }, + _changed: true, }, - } + }) ); _options({ number: 3, nested: { boolean: true } }); _update(true); expect(updateFn).toBeCalledTimes(5); - expect(updateFn).toBeCalledWith( - { - number: 3, - string: 'hi', - nested: { - boolean: true, - number: 0, + expect(updateFn).toHaveBeenLastCalledWith( + expect.objectContaining({ + number: expect.objectContaining({ + _value: 3, + _changed: true, + }), + 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: { - string: 'hihihihi', - boolean: false, + _value: { + 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, + }), + }) ); }); }); diff --git a/packages/overlayscrollbars/tests/jsdom/support/cache/cache.test.ts b/packages/overlayscrollbars/tests/jsdom/support/cache/cache.test.ts index 2ac3669..fdfe3d4 100644 --- a/packages/overlayscrollbars/tests/jsdom/support/cache/cache.test.ts +++ b/packages/overlayscrollbars/tests/jsdom/support/cache/cache.test.ts @@ -13,7 +13,7 @@ const createUpdater = (updaterReturn: (i: number) => T) => { }; describe('cache', () => { - describe('createCache', () => { + describe('cache with cacheUpdateInfo object', () => { test('creates and updates simple cache', () => { interface Test { number: number; @@ -33,69 +33,81 @@ describe('cache', () => { object: updateObj, }); - expect(updateCache('number').number).toBe(1); + expect(updateCache('number').number._value).toBe(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).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).toHaveBeenCalledWith(2, 1); + expect(updateNumberFn).toHaveBeenLastCalledWith(2, 1); let { string, boolean, object, number } = updateCache('number'); - expect(string).toBe(undefined); - expect(boolean).toBe(undefined); - expect(object).toBe(undefined); - expect(number).toBe(4); + expect(string._value).toBe(undefined); + expect(string._changed).toBe(false); + expect(boolean._value).toBe(undefined); + 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(updateStringFn).not.toHaveBeenCalled(); expect(updateObjFn).not.toHaveBeenCalled(); ({ string, boolean, object, number } = updateCache(['string', 'boolean', 'object'])); - expect(string).toBe('1'); - expect(boolean).toBe(!!(1 % 2)); - expect(object).toEqual({ 1: 1 }); - expect(number).toBe(undefined); + expect(string._value).toBe('1'); + expect(string._changed).toBe(true); + expect(boolean._value).toBe(!!(1 % 2)); + 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).toHaveBeenCalledWith(undefined, undefined); + expect(updateBooleanFn).toHaveBeenLastCalledWith(undefined, undefined); expect(updateStringFn).toHaveBeenCalledTimes(1); - expect(updateStringFn).toHaveBeenCalledWith(undefined, undefined); + expect(updateStringFn).toHaveBeenLastCalledWith(undefined, undefined); expect(updateObjFn).toHaveBeenCalledTimes(1); - expect(updateObjFn).toHaveBeenCalledWith(undefined, undefined); + expect(updateObjFn).toHaveBeenLastCalledWith(undefined, undefined); updateCache(['string', 'boolean', 'object']); expect(updateBooleanFn).toHaveBeenCalledTimes(2); - expect(updateBooleanFn).toHaveBeenCalledWith(!!(1 % 2), undefined); + expect(updateBooleanFn).toHaveBeenLastCalledWith(!!(1 % 2), undefined); expect(updateStringFn).toHaveBeenCalledTimes(2); - expect(updateStringFn).toHaveBeenCalledWith('1', undefined); + expect(updateStringFn).toHaveBeenLastCalledWith('1', undefined); expect(updateObjFn).toHaveBeenCalledTimes(2); - expect(updateObjFn).toHaveBeenCalledWith({ 1: 1 }, undefined); + expect(updateObjFn).toHaveBeenLastCalledWith({ 1: 1 }, undefined); updateCache(['string', 'boolean', 'object']); expect(updateBooleanFn).toHaveBeenCalledTimes(3); - expect(updateBooleanFn).toHaveBeenCalledWith(!!(2 % 2), !!(1 % 2)); + expect(updateBooleanFn).toHaveBeenLastCalledWith(!!(2 % 2), !!(1 % 2)); expect(updateStringFn).toHaveBeenCalledTimes(3); - expect(updateStringFn).toHaveBeenCalledWith('2', '1'); + expect(updateStringFn).toHaveBeenLastCalledWith('2', '1'); expect(updateObjFn).toHaveBeenCalledTimes(3); - expect(updateObjFn).toHaveBeenCalledWith({ 2: 2 }, { 1: 1 }); + expect(updateObjFn).toHaveBeenLastCalledWith({ 2: 2 }, { 1: 1 }); updateCache(['string', 'boolean', 'object']); ({ string, boolean, object, number } = updateCache()); - expect(string).toBe('5'); - expect(boolean).toBe(!!(5 % 2)); - expect(object).toEqual({ 5: 5 }); - expect(number).toBe(5); + expect(string._value).toBe('5'); + expect(string._changed).toBe(true); + expect(boolean._value).toBe(!!(5 % 2)); + 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(updateStringFn).toHaveBeenCalledTimes(5); @@ -109,17 +121,24 @@ describe('cache', () => { number: updateNumber, }); - expect(updateCache('number').number).toBe(0); - expect(updateNumberFn).toHaveBeenCalledWith(undefined, undefined); + let { _value, _changed } = updateCache('number').number; + expect(_value).toBe(0); + expect(_changed).toBe(true); + expect(updateNumberFn).toHaveBeenLastCalledWith(undefined, undefined); - expect(updateCache('number').number).toBe(undefined); - expect(updateNumberFn).toHaveBeenCalledWith(0, undefined); + ({ _value, _changed } = updateCache('number').number); + expect(_value).toBe(0); + expect(_changed).toBe(false); + expect(updateNumberFn).toHaveBeenLastCalledWith(0, undefined); - expect(updateCache('number').number).toBe(undefined); - expect(updateNumberFn).toHaveBeenCalledWith(0, 0); + ({ _value, _changed } = updateCache('number').number); + expect(_value).toBe(0); + expect(_changed).toBe(false); + expect(updateNumberFn).toHaveBeenLastCalledWith(0, 0); const changed = updateCache('number'); 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', () => { @@ -136,45 +155,91 @@ describe('cache', () => { ], }); - expect(updateCache('constObj').constObj).toBe(constObj); - expect(updateConstObjFn).toHaveBeenCalledWith(undefined, undefined); - expect(updateCache('constObj').constObj).toBe(undefined); - expect(updateConstObjFn).toHaveBeenCalledWith(constObj, undefined); - expect(updateCache('constObj').constObj).toBe(undefined); - expect(updateConstObjFn).toHaveBeenCalledWith(constObj, constObj); - expect(Object.prototype.hasOwnProperty.call(updateCache('constObj'), 'constObj')).toBe(false); + let { _value, _changed } = updateCache('constObj').constObj; + expect(_value).toEqual(constObj); + expect(_changed).toBe(true); + expect(updateConstObjFn).toHaveBeenLastCalledWith(undefined, undefined); - expect(updateCache('similarObj').similarObj).toEqual(constObj); - expect(updateSimilarObjFn).toHaveBeenCalledWith(undefined, undefined); - expect(updateCache('similarObj').similarObj).toEqual(constObj); - expect(updateSimilarObjFn).toHaveBeenCalledWith(constObj, undefined); - expect(updateCache('similarObj').similarObj).toEqual(constObj); - expect(updateSimilarObjFn).toHaveBeenCalledWith(constObj, constObj); - expect(Object.prototype.hasOwnProperty.call(updateCache('similarObj'), 'similarObj')).toBe(true); + ({ _value, _changed } = updateCache('constObj').constObj); + expect(_value).toEqual(constObj); + expect(_changed).toBe(false); + expect(updateConstObjFn).toHaveBeenLastCalledWith(constObj, undefined); - expect(updateCache('comparisonObj').comparisonObj).toEqual(constObj); - expect(updateComparisonObjFn).toHaveBeenCalledWith(undefined, undefined); - expect(updateCache('comparisonObj').comparisonObj).toBe(undefined); - expect(updateComparisonObjFn).toHaveBeenCalledWith(constObj, undefined); - expect(updateCache('comparisonObj').comparisonObj).toBe(undefined); - expect(updateComparisonObjFn).toHaveBeenCalledWith(constObj, constObj); - expect(Object.prototype.hasOwnProperty.call(updateCache('comparisonObj'), 'comparisonObj')).toBe(false); + ({ _value, _changed } = updateCache('constObj').constObj); + expect(_value).toEqual(constObj); + expect(_changed).toBe(false); + expect(updateConstObjFn).toHaveBeenLastCalledWith(constObj, constObj); + + ({ _value, _changed } = updateCache('similarObj').similarObj); + 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', () => { const [updateNumberFn, updateNumber] = createUpdater(() => 0); + const [, updateString] = createUpdater(() => 0); const updateCache = createCache({ number: updateNumber, + string: updateString, }); - expect(updateCache('number', true).number).toBe(0); - expect(updateNumberFn).toHaveBeenCalledWith(undefined, undefined); + let { _value, _changed } = updateCache('number', true).number; + expect(_value).toBe(0); + expect(_changed).toBe(true); + expect(updateNumberFn).toHaveBeenLastCalledWith(undefined, undefined); - expect(updateCache('number', true).number).toBe(0); - expect(updateNumberFn).toHaveBeenCalledWith(0, undefined); + ({ _value, _changed } = updateCache('number', true).number); + expect(_value).toBe(0); + expect(_changed).toBe(true); + expect(updateNumberFn).toHaveBeenLastCalledWith(0, undefined); - expect(updateCache('number', true).number).toBe(0); - expect(updateNumberFn).toHaveBeenCalledWith(0, 0); + ({ _value, _changed } = updateCache('number', true).number); + 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', () => { @@ -185,21 +250,35 @@ describe('cache', () => { number: [updateNumber, () => true], }); - expect(updateCache('string').string).toBe('hi'); - expect(updateStringFn).toHaveBeenCalledWith(undefined, undefined); - expect(updateCache('string').string).toBe('hi'); - expect(updateStringFn).toHaveBeenCalledWith('hi', undefined); - expect(updateCache('string').string).toBe('hi'); - expect(updateStringFn).toHaveBeenCalledWith('hi', 'hi'); - expect(Object.prototype.hasOwnProperty.call(updateCache('string'), 'string')).toBe(true); + let { _value, _changed } = updateCache('string').string; + expect(_value).toBe('hi'); + expect(_changed).toBe(true); + expect(updateStringFn).toHaveBeenLastCalledWith(undefined, undefined); - expect(updateCache('number').number).toBe(undefined); - expect(updateNumberFn).toHaveBeenCalledWith(undefined, undefined); - expect(updateCache('number').number).toBe(undefined); - expect(updateNumberFn).toHaveBeenCalledWith(1, undefined); - expect(updateCache('number').number).toBe(undefined); - expect(updateNumberFn).toHaveBeenCalledWith(2, 1); - expect(Object.prototype.hasOwnProperty.call(updateCache('number'), 'number')).toBe(false); + ({ _value, _changed } = updateCache('string').string); + expect(_value).toBe('hi'); + expect(_changed).toBe(true); + expect(updateStringFn).toHaveBeenLastCalledWith('hi', undefined); + + ({ _value, _changed } = updateCache('string').string); + 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', () => { @@ -211,12 +290,83 @@ describe('cache', () => { }); updateCache(); - expect(updateNumberFn).toHaveBeenCalledWith(undefined, undefined); - expect(updateNumberFn2).toHaveBeenCalledWith(undefined, undefined); + expect(updateNumberFn).toHaveBeenLastCalledWith(undefined, undefined); + expect(updateNumberFn2).toHaveBeenLastCalledWith(undefined, undefined); updateCache(null); - expect(updateNumberFn).toHaveBeenCalledWith(1, undefined); - expect(updateNumberFn2).toHaveBeenCalledWith(1, undefined); + expect(updateNumberFn).toHaveBeenLastCalledWith(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(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); }); }); });