├── .babelrc ├── .eslintrc ├── .gitignore ├── .npmignore ├── .npmrc ├── .prettierrc.js ├── .release-it.beta.json ├── .release-it.json ├── CHANGELOG.md ├── DEV_ONLY ├── App.tsx ├── calculate.ts ├── deepEqual.ts ├── default.ts ├── environment.ts ├── index.ts ├── maxAge.ts ├── maxArgs.ts ├── promise.ts ├── react.tsx ├── serialize.ts ├── shallowEqual.ts └── transformArgs.ts ├── LICENSE ├── README.md ├── __tests__ ├── compose.ts ├── deepEqual.ts ├── default.ts ├── infinite.ts ├── isMoized.ts ├── matchesArg.ts ├── matchesKey.ts ├── maxAge.ts ├── maxArgs.ts ├── promise.ts ├── react.tsx ├── serialize.ts ├── shallowEqual.ts ├── stats.ts ├── transformArgs.ts └── updateCacheForKey.ts ├── benchmark ├── Benchmark results.ods ├── addy-osmani.js ├── benchmark_results.csv └── index.js ├── docs ├── .nojekyll ├── assets │ ├── highlight.css │ ├── main.js │ ├── search.js │ └── style.css ├── functions │ ├── default-1.html │ ├── default.compose.html │ ├── default.isCollectingStats.html │ ├── default.isMoized.html │ ├── default.matchesArg.html │ ├── default.matchesKey.html │ ├── default.maxArgs.html │ ├── default.maxSize.html │ ├── default.profile.html │ ├── default.serializeWith.html │ ├── default.transformArgs.html │ └── default.updateCacheForKey.html ├── index.html ├── modules.html ├── modules │ └── default.html └── variables │ ├── default.clearStats.html │ ├── default.collectStats.html │ ├── default.deep.html │ ├── default.getStats.html │ ├── default.infinite.html │ ├── default.maxAge.html │ ├── default.promise.html │ ├── default.react.html │ ├── default.serialize.html │ └── default.shallow.html ├── es-to-mjs.js ├── img ├── multiple-parameters.png ├── overall-average.png └── single-parameter.png ├── index.d.ts ├── jest.config.js ├── jest.init.js ├── mjs-test.mjs ├── package.json ├── rollup.config.js ├── src ├── component.ts ├── constants.ts ├── index.ts ├── instance.ts ├── maxAge.ts ├── maxArgs.ts ├── options.ts ├── serialize.ts ├── stats.ts ├── updateCacheForKey.ts └── utils.ts ├── tsconfig.json ├── webpack └── webpack.config.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "lib": { 4 | "presets": [ 5 | [ 6 | "@babel/preset-env", 7 | { 8 | "loose": true 9 | } 10 | ] 11 | ] 12 | }, 13 | "test": { 14 | "plugins": ["@babel/plugin-proposal-class-properties"], 15 | "presets": [ 16 | [ 17 | "@babel/preset-env", 18 | { 19 | "loose": true 20 | } 21 | ], 22 | "@babel/preset-react" 23 | ] 24 | } 25 | }, 26 | "plugins": [ 27 | ["@babel/plugin-proposal-class-properties", { "loose": false }], 28 | ["@babel/plugin-proposal-private-methods", { "loose": false }], 29 | [ 30 | "@babel/plugin-proposal-private-property-in-object", 31 | { "loose": false } 32 | ] 33 | ], 34 | "presets": [ 35 | "@babel/preset-typescript", 36 | [ 37 | "@babel/preset-env", 38 | { 39 | "loose": true, 40 | "modules": false 41 | } 42 | ] 43 | ] 44 | } 45 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "node": true, 5 | "es6": true 6 | }, 7 | "extends": [ 8 | "eslint:recommended", 9 | "plugin:@typescript-eslint/eslint-recommended", 10 | "plugin:@typescript-eslint/recommended", 11 | "plugin:react/recommended" 12 | ], 13 | "parser": "@typescript-eslint/parser", 14 | "parserOptions": { 15 | "ecmaVersion": 2019, 16 | "sourceType": "module" 17 | }, 18 | "plugins": ["@typescript-eslint"], 19 | "rules": { 20 | "no-prototype-builtins": 0, 21 | "prefer-rest-params": 0, 22 | "prefer-spread": 0, 23 | 24 | "@typescript-eslint/ban-ts-comment": 0, 25 | "@typescript-eslint/explicit-function-return-type": 0, 26 | "@typescript-eslint/explicit-module-boundary-types": 0, 27 | "@typescript-eslint/no-empty-function": 0, 28 | "@typescript-eslint/no-explicit-any": 0, 29 | "@typescript-eslint/no-unused-vars": [ 30 | 2, 31 | { 32 | "ignoreRestSiblings": true 33 | } 34 | ] 35 | }, 36 | "settings": { 37 | "react": { 38 | "version": "detect" 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .nyc_output 3 | coverage 4 | dist 5 | node_modules 6 | lib 7 | es 8 | mjs 9 | *.log 10 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .babelrc 2 | .eslintrc 3 | .flowconfig 4 | .gitignore 5 | .idea 6 | .nyc_output 7 | benchmark 8 | coverage 9 | DEV_ONLY 10 | docs 11 | img 12 | node_modules 13 | test 14 | webpack 15 | jsdoc.config.json 16 | rollup.config.js 17 | yarn-error.log 18 | yarn.lock 19 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | scripts-prepend-node-path=true -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | trailingComma: 'es5', 3 | semi: true, 4 | singleQuote: true, 5 | tabWidth: 4, 6 | }; 7 | -------------------------------------------------------------------------------- /.release-it.beta.json: -------------------------------------------------------------------------------- 1 | { 2 | "github": { 3 | "release": true 4 | }, 5 | "npm": { 6 | "tag": "next" 7 | }, 8 | "preReleaseId": "beta", 9 | "hooks": { 10 | "before:init": [ 11 | "npm run lint", 12 | "npm run typecheck", 13 | "npm run test", 14 | "npm run dist", 15 | "npm run copy:mjs" 16 | ] 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /.release-it.json: -------------------------------------------------------------------------------- 1 | { 2 | "github": { 3 | "release": true, 4 | "tagName": "v${version}" 5 | }, 6 | "hooks": { 7 | "before:init": [ 8 | "npm run lint", 9 | "npm run typecheck", 10 | "npm run test", 11 | "npm run dist", 12 | "npm run copy:mjs" 13 | ], 14 | "before:release": [ 15 | "npm run docs", 16 | "git add .", 17 | "git commit -m 'Update docs'", 18 | "git push" 19 | ] 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /DEV_ONLY/App.tsx: -------------------------------------------------------------------------------- 1 | import moize from '../src/index'; 2 | 3 | moize.collectStats(); 4 | 5 | const method = function (one: string, two: string) { 6 | console.log('standard method fired', one, two); 7 | 8 | return [one, two].join(' '); 9 | }; 10 | 11 | const foo = 'foo'; 12 | const bar = 'bar'; 13 | 14 | console.group('expiration'); 15 | 16 | const expiringMemoized = moize.maxAge(1000)(method, { 17 | onExpire: (() => { 18 | let count = 0; 19 | 20 | return () => { 21 | if (count !== 0) { 22 | console.log( 23 | 'Expired! This is the last time I will fire, and this should be empty:', 24 | expiringMemoized.expirationsSnapshot 25 | ); 26 | 27 | console.log(moize.getStats()); 28 | 29 | return true; 30 | } 31 | 32 | console.log( 33 | 'Expired! I will now reset the expiration, but this should be empty:', 34 | expiringMemoized.expirationsSnapshot 35 | ); 36 | 37 | count++; 38 | 39 | return false; 40 | }; 41 | })(), 42 | updateExpire: true, 43 | }); 44 | 45 | expiringMemoized(foo, bar); 46 | expiringMemoized(foo, bar); 47 | expiringMemoized(foo, bar); 48 | expiringMemoized(foo, bar); 49 | expiringMemoized(foo, bar); 50 | expiringMemoized(foo, bar); 51 | expiringMemoized(foo, bar); 52 | 53 | console.log('existing expirations', expiringMemoized.expirationsSnapshot); 54 | 55 | console.groupEnd(); 56 | -------------------------------------------------------------------------------- /DEV_ONLY/calculate.ts: -------------------------------------------------------------------------------- 1 | import { get, isEmpty, isNil, omitBy, set, union } from 'lodash'; 2 | import moize from '../src'; 3 | import { log, logCache } from './environment'; 4 | 5 | const AGG_TYPE = { 6 | AVG: 'AVG', 7 | COUNT: 'COUNT', 8 | INCLUSIVE: 'INCLUSIVE', 9 | SUM: 'SUM', 10 | }; 11 | 12 | const { INCLUSIVE, SUM, AVG, COUNT } = AGG_TYPE; 13 | 14 | export const getFieldValue = (item, field) => { 15 | let value = typeof field === 'string' ? item[field] : get(item, field); 16 | 17 | if (typeof value === 'object' && value !== null) { 18 | value = value.value; 19 | } 20 | 21 | return value; 22 | }; 23 | 24 | export const setFieldValue = (item, field, value) => { 25 | if (typeof field === 'string') { 26 | item[field] = value; 27 | } else { 28 | set(item, field, value); 29 | } 30 | }; 31 | 32 | export const getFieldAggValue = (item, field) => { 33 | const value = typeof field === 'string' ? item[field] : get(item, field); 34 | 35 | if (typeof value === 'object' && value !== null) { 36 | return value.agg; 37 | } 38 | 39 | return undefined; 40 | }; 41 | 42 | export const unionInclusiveObject = (objA, objB) => { 43 | if (isNil(objA) && isNil(objB)) { 44 | return undefined; 45 | } else if (isNil(objA) && !isNil(objB)) { 46 | return typeof objB === 'object' ? { ...objB } : undefined; 47 | } else if (!isNil(objA) && isNil(objB)) { 48 | return typeof objA === 'object' ? { ...objA } : undefined; 49 | } else if (typeof objA !== 'object' || typeof objB !== 'object') { 50 | return undefined; 51 | } 52 | 53 | const sum = {}; 54 | const objAKeys = Object.keys(objA); 55 | const objBKeys = Object.keys(objB); 56 | const keys = union(objAKeys, objBKeys); 57 | 58 | keys.forEach((key) => { 59 | sum[key] = (objA[key] || 0) + (objB[key] || 0); 60 | }); 61 | 62 | return sum; 63 | }; 64 | 65 | export const sumWithNil = (a, b) => { 66 | if (isNil(a) && isNil(b)) { 67 | return undefined; 68 | } else if (isNil(a) && !isNil(b)) { 69 | return b; 70 | } else if (!isNil(a) && isNil(b)) { 71 | return a; 72 | } 73 | 74 | return a + b; 75 | }; 76 | 77 | // export const aggregateData = memoize( 78 | // export const aggregateData = memoizee( 79 | export const aggregateData = moize( 80 | (data, aggMetadata) => { 81 | const AGG_NAMES = Object.keys(aggMetadata); 82 | const { children } = data; 83 | 84 | if (isEmpty(children)) { 85 | return data; 86 | } 87 | 88 | // Recursion 89 | const newChildren = children.map((row) => 90 | row.children ? aggregateData(row, aggMetadata) : row 91 | ); 92 | 93 | const aggData = {}; 94 | 95 | AGG_NAMES.forEach((aggName) => { 96 | const { 97 | ref: field = aggName, 98 | type, 99 | rounding, 100 | } = aggMetadata[aggName]; 101 | 102 | let inclusiveAgg; 103 | 104 | let sumAgg; 105 | 106 | let avgAgg; 107 | 108 | let countAgg; 109 | 110 | newChildren.forEach((row) => { 111 | const rowAggValue = getFieldAggValue(row, field); 112 | const rowValue = getFieldValue(row, field); 113 | 114 | // Determine if it aggregates on aggregated value or on primitive value 115 | // Aggregate on aggregated: group row aggregated from group rows 116 | // Aggregate on primitive: group row aggregated from element rows 117 | 118 | const isAggOnAgg = !isNil(rowAggValue); 119 | const canAggOnPri = !isNil(rowValue); 120 | 121 | if (isAggOnAgg) { 122 | countAgg = sumWithNil(countAgg, rowAggValue[COUNT]); 123 | } else if (canAggOnPri) { 124 | countAgg = sumWithNil(countAgg, 1); 125 | } 126 | 127 | if (type === INCLUSIVE) { 128 | if (isAggOnAgg) { 129 | inclusiveAgg = unionInclusiveObject( 130 | inclusiveAgg, 131 | rowAggValue[INCLUSIVE] 132 | ); 133 | } else if (canAggOnPri) { 134 | inclusiveAgg = unionInclusiveObject(inclusiveAgg, { 135 | [rowValue]: 1, 136 | }); 137 | } 138 | } else if (type === SUM || type === AVG) { 139 | if (isAggOnAgg) { 140 | sumAgg = sumWithNil(sumAgg, rowAggValue[SUM]); 141 | } else if (canAggOnPri) { 142 | sumAgg = sumWithNil; 143 | 144 | // document.body.style.margin(sumAgg, rowValue); 145 | } 146 | } 147 | }); 148 | 149 | if (type === AVG && countAgg && !isNil(sumAgg)) { 150 | avgAgg = sumAgg / countAgg; 151 | if (rounding) { 152 | const { strategy } = rounding; 153 | 154 | if (strategy === 'truncate') { 155 | avgAgg = Math.trunc(avgAgg); 156 | } 157 | } 158 | } 159 | 160 | setFieldValue(aggData, field, { 161 | agg: omitBy( 162 | { 163 | [AVG]: avgAgg, 164 | [COUNT]: countAgg, 165 | [INCLUSIVE]: inclusiveAgg, 166 | [SUM]: sumAgg, 167 | }, 168 | isNil 169 | ), 170 | }); 171 | }); 172 | 173 | return { 174 | ...data, 175 | ...aggData, 176 | children: newChildren, 177 | }; 178 | }, 179 | { profileName: 'aggregateData' } 180 | ); 181 | 182 | const aggMetadata = { 183 | a: { type: COUNT }, 184 | b: { type: SUM }, 185 | c: { type: AVG }, 186 | d: { type: INCLUSIVE }, 187 | }; 188 | const el1 = { 189 | a: 'sku_1', 190 | b: 1, 191 | c: 2, 192 | d: 'cat_1', 193 | }; 194 | const el2 = { 195 | a: 'sku_2', 196 | b: 3, 197 | c: 4, 198 | d: 'cat_1', 199 | }; 200 | const el3 = { 201 | a: 'sku_3', 202 | b: 5, 203 | c: undefined, 204 | d: 'cat_2', 205 | }; 206 | const el4 = { 207 | a: 'sku_4', 208 | b: null, 209 | c: 0, 210 | d: 'cat_2', 211 | }; 212 | const el5 = { 213 | a: 'sku_5', 214 | b: undefined, 215 | c: null, 216 | d: 'cat_2', 217 | }; 218 | 219 | const dataToAggregate = { 220 | children: [{ children: [el1, el2] }, { children: [el3, el4, el5] }], 221 | }; 222 | 223 | const calc = moize( 224 | (object, metadata) => 225 | Object.keys(object).reduce((totals, key) => { 226 | if (Array.isArray(object[key])) { 227 | totals[key] = object[key].map((subObject) => 228 | calc(subObject, metadata) 229 | ); 230 | } else { 231 | totals[key] = object[key].a + object[key].b + metadata.c; 232 | } 233 | 234 | return totals; 235 | }, {}), 236 | { profileName: 'calc', maxSize: 5 } 237 | ); 238 | 239 | const data = { 240 | fifth: { 241 | a: 4, 242 | b: 5, 243 | }, 244 | first: [ 245 | { 246 | second: { 247 | a: 1, 248 | b: 2, 249 | }, 250 | }, 251 | { 252 | third: [ 253 | { 254 | fourth: { 255 | a: 2, 256 | b: 3, 257 | }, 258 | }, 259 | ], 260 | }, 261 | ], 262 | }; 263 | const metadata = { 264 | c: 6, 265 | }; 266 | 267 | export function aggregate() { 268 | const aggData1 = aggregateData(dataToAggregate, aggMetadata); 269 | const aggData2 = aggregateData(dataToAggregate, aggMetadata); 270 | 271 | log( 272 | 'are aggregations equal', 273 | [dataToAggregate, aggMetadata], 274 | aggData1 === aggData2 275 | ); 276 | 277 | return aggregateData; 278 | } 279 | 280 | export function calculate() { 281 | const result1 = calc(data, metadata); 282 | const result2 = calc(data, metadata); 283 | 284 | log('result 1', [data, metadata], result1); 285 | log('result 2', [data, metadata], result2); 286 | log( 287 | 'are complex calculations equal', 288 | [data, metadata], 289 | result1 === result2 290 | ); 291 | 292 | logCache(calc); 293 | 294 | return calc; 295 | } 296 | -------------------------------------------------------------------------------- /DEV_ONLY/deepEqual.ts: -------------------------------------------------------------------------------- 1 | import moize from '../src'; 2 | import { logCache, logStoredValue } from './environment'; 3 | 4 | type Arg = { 5 | one: number; 6 | two: { 7 | deep: number; 8 | }; 9 | }; 10 | 11 | function method({ one, two }: Arg) { 12 | console.log('deep equal fired', one, two); 13 | 14 | return [one, two]; 15 | } 16 | 17 | const memoized = moize.deep(method); 18 | 19 | export function deepEqual() { 20 | memoized({ one: 1, two: { deep: 2 } }); 21 | 22 | logStoredValue(memoized, 'exists', [{ one: 1, two: { deep: 2 } }]); 23 | logStoredValue(memoized, 'does not exist', [{ one: 1, two: { three: 3 } }]); 24 | 25 | logCache(memoized); 26 | 27 | return memoized; 28 | } 29 | -------------------------------------------------------------------------------- /DEV_ONLY/default.ts: -------------------------------------------------------------------------------- 1 | import moize from '../src'; 2 | import { log, logCache, logStoredValue } from './environment'; 3 | 4 | function method(one: string, two: string) { 5 | console.log('standard method fired', one, two); 6 | 7 | return [one, two].join('|_|'); 8 | } 9 | 10 | function methodDefaulted(one: string, two = 'default') { 11 | console.log('defaulted method fired', one, two); 12 | 13 | return [one, two].join('|_|'); 14 | } 15 | 16 | const memoized = moize.infinite(method); 17 | const memoizedDefaulted = moize.infinite(methodDefaulted); 18 | 19 | const foo = 'foo'; 20 | const bar = 'bar'; 21 | 22 | export function standard() { 23 | memoized(foo, bar); 24 | logStoredValue(memoized, 1, [foo, bar]); 25 | 26 | logCache(memoized); 27 | 28 | return memoized; 29 | } 30 | 31 | export function addEntry() { 32 | memoized(foo, bar); 33 | logStoredValue(memoized, 1, [foo, bar]); 34 | 35 | memoized.set([bar, foo], 'something totally different'); 36 | 37 | logStoredValue(memoized, 'after add', [bar, foo]); 38 | 39 | logCache(memoized); 40 | 41 | return memoized; 42 | } 43 | 44 | export function getEntry() { 45 | const result = memoized(foo, bar); 46 | logStoredValue(memoized, 1, [foo, bar]); 47 | 48 | logStoredValue(memoized, 'exists', [foo, bar]); 49 | logStoredValue(memoized, 'is undefined if not stored', [bar, foo]); 50 | 51 | log('is strictly equal', [foo, bar], result === memoized.get([foo, bar])); 52 | 53 | logCache(memoized); 54 | 55 | return memoized; 56 | } 57 | 58 | export function hasEntry() { 59 | memoized(foo, bar); 60 | logStoredValue(memoized, 1, [foo, bar]); 61 | 62 | log('is stored', [foo, bar], memoized.has([foo, bar])); 63 | log('is not stored', [bar, foo], memoized.has([bar, foo])); 64 | 65 | logCache(memoized); 66 | 67 | return memoized; 68 | } 69 | 70 | export function updateEntry() { 71 | memoized(foo, bar); 72 | logStoredValue(memoized, 1, [foo, bar]); 73 | 74 | memoized(bar, foo); 75 | logStoredValue(memoized, 2, [bar, foo]); 76 | 77 | memoized.set([foo, bar], 'something totally different'); 78 | logStoredValue(memoized, 'after update', [foo, bar]); 79 | 80 | logCache(memoized); 81 | 82 | return memoized; 83 | } 84 | 85 | export function cacheEntries() { 86 | memoized(foo, bar); 87 | 88 | const { cacheSnapshot } = memoized; 89 | 90 | console.log('keys', cacheSnapshot.keys); 91 | console.log('values', cacheSnapshot.values); 92 | 93 | logCache(memoized); 94 | 95 | return memoized; 96 | } 97 | 98 | export function withDefaultParams() { 99 | memoizedDefaulted(foo, bar); 100 | memoizedDefaulted(foo); 101 | memoizedDefaulted(foo); 102 | memoizedDefaulted(foo, bar); 103 | 104 | logStoredValue(memoizedDefaulted, 'with value', [foo, bar]); 105 | logStoredValue(memoizedDefaulted, 'with default', [foo]); 106 | 107 | logCache(memoizedDefaulted); 108 | 109 | return memoizedDefaulted; 110 | } 111 | -------------------------------------------------------------------------------- /DEV_ONLY/environment.ts: -------------------------------------------------------------------------------- 1 | import 'core-js'; 2 | import 'regenerator-runtime/runtime'; 3 | 4 | import moize from '../src'; 5 | import { type Key, type Moized } from '../index.d'; 6 | 7 | moize.collectStats(); 8 | 9 | export function log(message: string, key: Key, value: any) { 10 | console.log(`result (${message})`, key, value); 11 | } 12 | 13 | export function logCache(memoized: Moized) { 14 | console.log('cache', memoized.cacheSnapshot); 15 | } 16 | 17 | export function logStoredValue( 18 | memoized: Moized, 19 | message: number | string, 20 | key: Key 21 | ) { 22 | console.log(`result (${message})`, key, memoized.get(key)); 23 | } 24 | 25 | export function createContainer() { 26 | const div = document.createElement('div'); 27 | 28 | div.textContent = 'Check the console for details.'; 29 | 30 | div.id = 'app-container'; 31 | div.style.backgroundColor = '#1d1d1d'; 32 | div.style.boxSizing = 'border-box'; 33 | div.style.color = '#d5d5d5'; 34 | div.style.height = '100vh'; 35 | div.style.padding = '15px'; 36 | div.style.width = '100vw'; 37 | 38 | document.body.style.margin = '0px'; 39 | document.body.style.padding = '0px'; 40 | 41 | document.body.appendChild(div); 42 | 43 | return div; 44 | } 45 | -------------------------------------------------------------------------------- /DEV_ONLY/index.ts: -------------------------------------------------------------------------------- 1 | import cloneDeep from 'lodash/cloneDeep'; 2 | import moize from '../src'; 3 | import { createContainer } from './environment'; 4 | 5 | import { type Moized } from '../index.d'; 6 | 7 | const container = createContainer(); 8 | 9 | function logStats(name: string, memoized: Moized) { 10 | console.groupCollapsed(`stats for ${name}`); 11 | 12 | console.log('global', cloneDeep(moize.getStats())); 13 | console.log( 14 | `specific to ${memoized.options.profileName}`, 15 | memoized.getStats() 16 | ); 17 | 18 | memoized.clear(); 19 | memoized.clearStats(); 20 | 21 | console.groupEnd(); 22 | } 23 | 24 | async function run() { 25 | const { aggregate, calculate } = await import('./calculate'); 26 | const { 27 | addEntry, 28 | cacheEntries, 29 | getEntry, 30 | hasEntry, 31 | standard, 32 | updateEntry, 33 | withDefaultParams, 34 | } = await import('./default'); 35 | const { deepEqual } = await import('./deepEqual'); 36 | const { maxAge } = await import('./maxAge'); 37 | const { maxArgs } = await import('./maxArgs'); 38 | const { bluebirdPromise, nativePromise } = await import('./promise'); 39 | const { renderSimple } = await import('./react'); 40 | const { serialize } = await import('./serialize'); 41 | const { shallowEqual } = await import('./shallowEqual'); 42 | const { transformArgs } = await import('./transformArgs'); 43 | 44 | const useCases: [string, (container: HTMLDivElement) => void | Moized][] = [ 45 | // default 46 | ['standard', standard], 47 | ['with default params', withDefaultParams], 48 | ['add new entry', addEntry], 49 | ['get existing entry', getEntry], 50 | ['has existing entry', hasEntry], 51 | ['update existing entry', updateEntry], 52 | ['keys and values', cacheEntries], 53 | 54 | // simple options 55 | ['deep equal', deepEqual], 56 | ['max age', maxAge], 57 | ['max args', maxArgs], 58 | ['promise (native)', nativePromise], 59 | ['promise (bluebird)', bluebirdPromise], 60 | ['react simple', renderSimple], 61 | ['serialize', serialize], 62 | ['shallow equal', shallowEqual], 63 | ['transform args', transformArgs], 64 | 65 | // complex computation 66 | ['aggregate', aggregate], 67 | ['calculate', calculate], 68 | ]; 69 | 70 | useCases.forEach(([name, useCase]) => { 71 | console.groupCollapsed(name); 72 | 73 | const memoized = useCase(container); 74 | 75 | if (memoized) { 76 | if (memoized.options.isPromise) { 77 | new Promise((resolve) => setTimeout(resolve, 100)).then(() => 78 | logStats(name, memoized) 79 | ); 80 | } else { 81 | logStats(name, memoized); 82 | } 83 | } 84 | 85 | console.groupEnd(); 86 | }); 87 | } 88 | 89 | run(); 90 | -------------------------------------------------------------------------------- /DEV_ONLY/maxAge.ts: -------------------------------------------------------------------------------- 1 | import moize from '../src'; 2 | import { logCache, logStoredValue } from './environment'; 3 | 4 | function method(one: string, two: string) { 5 | console.log('max age fired', one, two); 6 | 7 | return [one, two].join('|_|'); 8 | } 9 | 10 | const memoized = moize.maxAge(1000)(method, { 11 | onExpire: (() => { 12 | let count = 0; 13 | 14 | return () => { 15 | if (count !== 0) { 16 | console.log( 17 | 'Expired! This is the last time I will fire, and this should be empty:', 18 | memoized.expirationsSnapshot 19 | ); 20 | 21 | console.log(moize.getStats()); 22 | 23 | return true; 24 | } 25 | 26 | console.log( 27 | 'Expired! I will now reset the expiration, but this should be empty:', 28 | memoized.expirationsSnapshot 29 | ); 30 | 31 | count++; 32 | 33 | return false; 34 | }; 35 | })(), 36 | updateExpire: true, 37 | }); 38 | 39 | const foo = 'foo'; 40 | const bar = 'bar'; 41 | 42 | export function maxAge() { 43 | memoized(foo, bar); 44 | memoized(foo, bar); 45 | memoized(foo, bar); 46 | memoized(foo, bar); 47 | memoized(foo, bar); 48 | memoized(foo, bar); 49 | memoized(foo, bar); 50 | 51 | console.log('existing expirations', memoized.expirationsSnapshot); 52 | 53 | logStoredValue(memoized, 'exists', [foo, bar]); 54 | 55 | logCache(memoized); 56 | 57 | return memoized; 58 | } 59 | -------------------------------------------------------------------------------- /DEV_ONLY/maxArgs.ts: -------------------------------------------------------------------------------- 1 | import moize from '../src'; 2 | import { logCache, logStoredValue } from './environment'; 3 | 4 | function method(one: string, two: string) { 5 | console.log('max args fired', one, two); 6 | 7 | return [one, two].join('|_|'); 8 | } 9 | 10 | const memoized = moize.maxArgs(1)(method); 11 | 12 | const foo = 'foo'; 13 | const bar = 'bar'; 14 | const baz = 'baz'; 15 | 16 | export function maxArgs() { 17 | memoized(foo, bar); 18 | memoized(foo, baz); 19 | 20 | logStoredValue(memoized, 'exists only for foo', [foo, bar]); 21 | 22 | logCache(memoized); 23 | 24 | return memoized; 25 | } 26 | -------------------------------------------------------------------------------- /DEV_ONLY/promise.ts: -------------------------------------------------------------------------------- 1 | import Bluebird from 'bluebird'; 2 | import moize from '../src'; 3 | import { type Moized } from '../index.d'; 4 | import { logCache, logStoredValue } from './environment'; 5 | 6 | function createMethod( 7 | type: string, 8 | method: 'resolve' | 'reject', 9 | PromiseLibrary: PromiseConstructor 10 | ) { 11 | if (method === 'reject') { 12 | return function (number: number, otherNumber: number) { 13 | console.log(`${type} promise reject fired`, number, otherNumber); 14 | 15 | return PromiseLibrary.reject( 16 | new Error(`rejected ${number * otherNumber}`) 17 | ); 18 | }; 19 | } 20 | 21 | return function (number: number, otherNumber: number) { 22 | console.log(`${type} promise fired`, number, otherNumber); 23 | 24 | return PromiseLibrary.resolve(number * otherNumber); 25 | }; 26 | } 27 | 28 | const bluebirdMemoized = moize.promise( 29 | createMethod( 30 | 'bluebird', 31 | 'resolve', 32 | Bluebird as unknown as PromiseConstructor 33 | ), 34 | { profileName: 'bluebird (reject)' } 35 | ); 36 | const bluebirdMemoizedReject = moize.promise( 37 | createMethod( 38 | 'bluebird', 39 | 'reject', 40 | Bluebird as unknown as PromiseConstructor 41 | ), 42 | { profileName: 'bluebird (reject)' } 43 | ); 44 | 45 | const nativeMemoized = moize.promise( 46 | createMethod('native', 'resolve', Promise), 47 | { 48 | profileName: 'native', 49 | } 50 | ); 51 | const nativeMemoizedReject = moize.promise( 52 | createMethod('native', 'reject', Promise), 53 | { 54 | profileName: 'native (reject)', 55 | } 56 | ); 57 | const nativeExpiring = moize.promise( 58 | createMethod('native', 'resolve', Promise), 59 | { 60 | maxAge: 1500, 61 | onCacheHit(cache) { 62 | console.log('resolved with', cache); 63 | }, 64 | onExpire() { 65 | console.log('expired'); 66 | }, 67 | profileName: 'native (expiring)', 68 | } 69 | ); 70 | 71 | function logItems(items: [number, number, Moized, string][]) { 72 | items.forEach(([number, otherNumber, method, name]) => { 73 | const key = [number, otherNumber]; 74 | 75 | method(number, otherNumber).then(() => { 76 | console.groupCollapsed(`delayed results for ${name}`); 77 | 78 | logStoredValue(method, 'exists', key); 79 | logStoredValue(method, 'does not exist', key.slice().reverse()); 80 | 81 | logCache(method); 82 | 83 | console.groupEnd(); 84 | }); 85 | }); 86 | } 87 | 88 | export function bluebirdPromise() { 89 | logItems([ 90 | [4, 9, bluebirdMemoized, 'bluebird (resolve)'], 91 | [7, 25, bluebirdMemoizedReject, 'bluebird (reject)'], 92 | ]); 93 | 94 | return bluebirdMemoized; 95 | } 96 | 97 | export function nativePromise() { 98 | logItems([ 99 | [6, 9, nativeMemoized, 'native (resolve)'], 100 | [21, 12, nativeMemoizedReject, 'native (reject)'], 101 | [13, 4, nativeExpiring, 'native (expiring)'], 102 | ]); 103 | 104 | return nativeMemoized; 105 | } 106 | -------------------------------------------------------------------------------- /DEV_ONLY/react.tsx: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React from 'react'; 3 | import ReactDOM from 'react-dom'; 4 | import moize from '../src'; 5 | import { type Moized } from '../index.d'; 6 | 7 | type Props = { 8 | bar?: string; 9 | fn: (...args: any[]) => any; 10 | key?: string; 11 | object?: Record; 12 | value?: any; 13 | }; 14 | 15 | function ValueBar({ bar, fn, object, value }: Props) { 16 | console.count('react'); 17 | console.log('react element fired', bar, fn, object, value); 18 | 19 | return ( 20 |
21 | {value} {bar} 22 |
23 | ); 24 | } 25 | 26 | ValueBar.propTypes = { 27 | bar: PropTypes.string.isRequired, 28 | fn: PropTypes.func.isRequired, 29 | object: PropTypes.object.isRequired, 30 | value: PropTypes.string.isRequired, 31 | }; 32 | 33 | ValueBar.defaultProps = { 34 | bar: 'default', 35 | }; 36 | 37 | const Memoized = moize.react(ValueBar); 38 | 39 | const foo = 'foo'; 40 | const bar = 'bar'; 41 | const baz = 'baz'; 42 | 43 | const data = [ 44 | { 45 | fn() { 46 | return foo; 47 | }, 48 | object: { value: foo }, 49 | value: foo, 50 | }, 51 | { 52 | bar, 53 | fn() { 54 | return bar; 55 | }, 56 | object: { value: bar }, 57 | value: bar, 58 | }, 59 | { 60 | fn() { 61 | return baz; 62 | }, 63 | object: { value: baz }, 64 | value: baz, 65 | }, 66 | ]; 67 | 68 | type SimpleAppProps = { 69 | isRerender?: boolean; 70 | }; 71 | 72 | class SimpleApp extends React.Component { 73 | MoizedComponent: Moized; 74 | 75 | componentDidUpdate() { 76 | console.log('post-update stats', this.MoizedComponent.getStats()); 77 | } 78 | 79 | setMoizedComponent = (Ref: { MoizedComponent: Moized }) => { 80 | this.MoizedComponent = Ref.MoizedComponent; 81 | }; 82 | 83 | render() { 84 | console.log('rendering simple app'); 85 | 86 | const { isRerender } = this.props; 87 | 88 | return ( 89 |
90 |

App

91 | 92 |
93 |

Memoized data list

94 | 95 | {data.map((values, index) => ( 96 | 102 | ))} 103 |
104 |
105 | ); 106 | } 107 | } 108 | 109 | export function renderSimple(container: HTMLDivElement) { 110 | const simpleAppContainer = document.createElement('div'); 111 | 112 | container.appendChild(simpleAppContainer); 113 | 114 | ReactDOM.render(, simpleAppContainer); 115 | 116 | setTimeout(() => { 117 | ReactDOM.render(, simpleAppContainer); 118 | }, 3000); 119 | } 120 | -------------------------------------------------------------------------------- /DEV_ONLY/serialize.ts: -------------------------------------------------------------------------------- 1 | import moize from '../src'; 2 | import { logCache, logStoredValue } from './environment'; 3 | 4 | type Arg = { 5 | one: number; 6 | two: number; 7 | three: () => void; 8 | four: symbol; 9 | five: null; 10 | }; 11 | 12 | function method({ one, two, three, four }: Arg) { 13 | return [one, two, three, four]; 14 | } 15 | 16 | const memoized = moize.serialize(method); 17 | 18 | export function serialize() { 19 | memoized({ one: 1, two: 2, three() {}, four: Symbol('foo'), five: null }); 20 | 21 | logStoredValue(memoized, 'exists', [ 22 | { one: 1, two: 2, three() {}, four: Symbol('foo'), five: null }, 23 | ]); 24 | logStoredValue(memoized, 'does not exist', [ 25 | { one: 1, two: 2, three: 3, four: 4 }, 26 | ]); 27 | 28 | logCache(memoized); 29 | 30 | return memoized; 31 | } 32 | -------------------------------------------------------------------------------- /DEV_ONLY/shallowEqual.ts: -------------------------------------------------------------------------------- 1 | import moize from '../src'; 2 | import { logCache, logStoredValue } from './environment'; 3 | 4 | type Arg = { 5 | one: number; 6 | two: number; 7 | }; 8 | 9 | function method({ one, two }: Arg) { 10 | console.log('deep equal fired', one, two); 11 | 12 | return [one, two]; 13 | } 14 | 15 | const memoized = moize.shallow(method); 16 | 17 | export function shallowEqual() { 18 | memoized({ one: 1, two: 2 }); 19 | 20 | logStoredValue(memoized, 'exists', [{ one: 1, two: 2 }]); 21 | logStoredValue(memoized, 'does not exist', [{ one: 1, two: 3 }]); 22 | 23 | logCache(memoized); 24 | 25 | return memoized; 26 | } 27 | -------------------------------------------------------------------------------- /DEV_ONLY/transformArgs.ts: -------------------------------------------------------------------------------- 1 | import moize from '../src'; 2 | import { logCache, logStoredValue } from './environment'; 3 | 4 | function method(one: string, two: string, three: string) { 5 | console.log('transform args fired', one, two, three); 6 | 7 | return [two, three]; 8 | } 9 | 10 | const memoized = moize(method, { 11 | transformArgs(args: string[]) { 12 | const newKey: string[] = []; 13 | 14 | let index = args.length; 15 | 16 | while (--index) { 17 | newKey[index - 1] = args[index]; 18 | } 19 | 20 | return newKey; 21 | }, 22 | }); 23 | 24 | const foo = 'foo'; 25 | const bar = 'bar'; 26 | const baz = 'baz'; 27 | 28 | export function transformArgs() { 29 | memoized(foo, bar, baz); 30 | 31 | logStoredValue(memoized, 'exists', [foo, bar, baz]); 32 | logStoredValue(memoized, 'exists for different first', [null, bar, baz]); 33 | 34 | logCache(memoized); 35 | 36 | return memoized; 37 | } 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Tony Quetano 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /__tests__/compose.ts: -------------------------------------------------------------------------------- 1 | import moize from '../src'; 2 | 3 | const foo = 'foo'; 4 | const bar = 'bar'; 5 | 6 | const method = jest.fn(function (one: string, two: string) { 7 | return { one, two }; 8 | }); 9 | 10 | describe('moize.compose', () => { 11 | it('should compose the moize methods into a new method with options combined', async () => { 12 | const maxSize = moize.maxSize(5); 13 | const maxAge = moize.maxAge(500); 14 | const serialize = moize.serialize; 15 | 16 | const composedMoizer = moize.compose(maxSize, maxAge, serialize); 17 | const composed = composedMoizer(method); 18 | 19 | expect(composed.options).toEqual( 20 | expect.objectContaining({ 21 | maxAge: 500, 22 | maxSize: 5, 23 | isSerialized: true, 24 | }) 25 | ); 26 | 27 | composed(foo, bar); 28 | 29 | expect(composed.cache.keys).toEqual([['|foo|bar|']]); 30 | 31 | await new Promise((resolve) => setTimeout(resolve, 1000)); 32 | 33 | expect(composed.cache.size).toBe(0); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /__tests__/deepEqual.ts: -------------------------------------------------------------------------------- 1 | import moize from '../src'; 2 | 3 | type Arg = { 4 | one: number; 5 | two: { 6 | deep: 2; 7 | }; 8 | }; 9 | 10 | const method = jest.fn(function ({ one, two }: Arg) { 11 | return [one, two.deep]; 12 | }); 13 | 14 | const memoized = moize.deep(method); 15 | 16 | describe('moize.deep', () => { 17 | it('should memoized based on the deep values', () => { 18 | const resultA = memoized({ one: 1, two: { deep: 2 } }); 19 | const resultB = memoized({ one: 1, two: { deep: 2 } }); 20 | 21 | expect(resultA).toEqual([1, 2]); 22 | expect(resultA).toBe(resultB); 23 | 24 | expect(method).toHaveBeenCalledTimes(1); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /__tests__/isMoized.ts: -------------------------------------------------------------------------------- 1 | import moize from '../src'; 2 | 3 | const method = jest.fn(function (one: string, two: string) { 4 | return { one, two }; 5 | }); 6 | 7 | describe('moize.isMoized', () => { 8 | it('should validate if the function passed is moized', () => { 9 | const memoized = moize(method); 10 | 11 | expect(moize.isMoized(method)).toBe(false); 12 | expect(moize.isMoized(memoized)).toBe(true); 13 | }); 14 | 15 | it('should handle random data types', () => { 16 | const types = [undefined, null, 'string', 123, [], {}]; 17 | 18 | types.forEach((type) => { 19 | expect(moize.isMoized(type)).toBe(false); 20 | }); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /__tests__/matchesArg.ts: -------------------------------------------------------------------------------- 1 | import moize from '../src'; 2 | 3 | const method = jest.fn(function (one: string, two?: string) { 4 | return { one, two }; 5 | }); 6 | 7 | function argMatcher(cacheKeyArg: string, keyArg: string) { 8 | return cacheKeyArg === 'foo' || keyArg === 'foo'; 9 | } 10 | 11 | const memoized = moize.matchesArg(argMatcher)(method); 12 | 13 | const foo = 'foo'; 14 | const bar = 'bar'; 15 | 16 | describe('moize.matchesArg', () => { 17 | it('performs a custom equality check of specific args in the key', () => { 18 | const resultA = memoized(foo, bar); 19 | const resultB = memoized(bar, foo); 20 | 21 | expect(resultA).toEqual({ one: foo, two: bar }); 22 | expect(resultB).toBe(resultA); 23 | 24 | expect(method).toHaveBeenCalledTimes(1); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /__tests__/matchesKey.ts: -------------------------------------------------------------------------------- 1 | import moize from '../src'; 2 | 3 | import type { Key } from '../index.d'; 4 | 5 | const method = jest.fn(function (one: string, two?: string, three?: string) { 6 | return { one, two, three }; 7 | }); 8 | 9 | function keyMatcher(_cacheKey: Key, key: Key) { 10 | return key.includes('foo') && !key.includes('quz'); 11 | } 12 | 13 | const memoized = moize.matchesKey(keyMatcher)(method); 14 | 15 | const foo = 'foo'; 16 | const bar = 'bar'; 17 | const baz = 'baz'; 18 | 19 | describe('moize.matchesKey', () => { 20 | it('performs a custom equality check of the key', () => { 21 | const resultA = memoized(foo, bar, baz); 22 | const resultB = memoized(foo); 23 | 24 | expect(resultA).toEqual({ one: foo, two: bar, three: baz }); 25 | expect(resultB).toBe(resultA); 26 | 27 | expect(method).toHaveBeenCalledTimes(1); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /__tests__/maxAge.ts: -------------------------------------------------------------------------------- 1 | import moize from '../src'; 2 | 3 | function method(one: string, two: string) { 4 | return [one, two]; 5 | } 6 | 7 | const foo = 'foo'; 8 | const bar = 'bar'; 9 | 10 | describe('moize.maxAge', () => { 11 | it('removes the item from cache after the time passed', async () => { 12 | const memoized = moize.maxAge(1000)(method, { 13 | onExpire: jest.fn(), 14 | }); 15 | 16 | memoized(foo, bar); 17 | 18 | expect(memoized.has([foo, bar])).toBe(true); 19 | expect(memoized.options.onExpire).not.toHaveBeenCalled(); 20 | 21 | await new Promise((resolve) => setTimeout(resolve, 1500)); 22 | 23 | expect(memoized.has([foo, bar])).toBe(false); 24 | expect(memoized.options.onExpire).toHaveBeenCalled(); 25 | }); 26 | 27 | it('notifies of cache change on removal if onCacheChange', async () => { 28 | const memoized = moize.maxAge(1000)(method, { 29 | onCacheChange: jest.fn(), 30 | }); 31 | 32 | memoized(foo, bar); 33 | 34 | expect(memoized.has([foo, bar])).toBe(true); 35 | 36 | await new Promise((resolve) => setTimeout(resolve, 1500)); 37 | 38 | expect(memoized.has([foo, bar])).toBe(false); 39 | 40 | expect(memoized.options.onCacheChange).toHaveBeenCalledWith( 41 | memoized.cache, 42 | memoized.options, 43 | memoized 44 | ); 45 | }); 46 | 47 | it('updates the expiration when called and cache is hit', async () => { 48 | const withUpdateExpire = moize.maxAge(1000, true)(method); 49 | 50 | withUpdateExpire(foo, bar); 51 | 52 | setTimeout(() => { 53 | expect(withUpdateExpire.has([foo, bar])).toBe(true); 54 | }, 1000); 55 | 56 | await new Promise((resolve) => setTimeout(resolve, 700)); 57 | 58 | withUpdateExpire(foo, bar); 59 | 60 | expect(withUpdateExpire.has([foo, bar])).toBe(true); 61 | 62 | await new Promise((resolve) => setTimeout(resolve, 1500)); 63 | 64 | expect(withUpdateExpire.has([foo, bar])).toBe(false); 65 | }); 66 | 67 | it('calls the onExpire method when the item is removed from cache', async () => { 68 | const onExpire = jest.fn(); 69 | 70 | const withOnExpire = moize.maxAge(1000, onExpire)(method); 71 | 72 | withOnExpire(foo, bar); 73 | 74 | expect(withOnExpire.has([foo, bar])).toBe(true); 75 | expect(withOnExpire.options.onExpire).not.toHaveBeenCalled(); 76 | 77 | await new Promise((resolve) => setTimeout(resolve, 1500)); 78 | 79 | expect(withOnExpire.has([foo, bar])).toBe(false); 80 | expect(withOnExpire.options.onExpire).toHaveBeenCalledTimes(1); 81 | }); 82 | 83 | it('updates the expiration timing and calls the onExpire method when the item is removed from cache', async () => { 84 | const onExpire = jest.fn(); 85 | 86 | const withExpireOptions = moize.maxAge(1000, { 87 | onExpire, 88 | updateExpire: true, 89 | })(method); 90 | 91 | withExpireOptions(foo, bar); 92 | 93 | setTimeout(() => { 94 | expect(withExpireOptions.has([foo, bar])).toBe(true); 95 | }, 1000); 96 | 97 | await new Promise((resolve) => setTimeout(resolve, 700)); 98 | 99 | withExpireOptions(foo, bar); 100 | 101 | await new Promise((resolve) => setTimeout(resolve, 1500)); 102 | 103 | expect(withExpireOptions.has([foo, bar])).toBe(false); 104 | expect(withExpireOptions.options.onExpire).toHaveBeenCalledTimes(1); 105 | }); 106 | 107 | it('allows the expiration to be re-established if onExpire returns false', async () => { 108 | const onExpire = jest 109 | .fn() 110 | .mockReturnValueOnce(false) 111 | .mockReturnValue(true); 112 | 113 | const withOnExpire = moize.maxAge(1000, onExpire)(method); 114 | 115 | withOnExpire(foo, bar); 116 | 117 | expect(withOnExpire.has([foo, bar])).toBe(true); 118 | expect(withOnExpire.options.onExpire).not.toHaveBeenCalled(); 119 | 120 | await new Promise((resolve) => setTimeout(resolve, 1100)); 121 | 122 | expect(withOnExpire.has([foo, bar])).toBe(true); 123 | expect(withOnExpire.options.onExpire).toHaveBeenCalledTimes(1); 124 | 125 | await new Promise((resolve) => setTimeout(resolve, 1100)); 126 | 127 | expect(withOnExpire.has([foo, bar])).toBe(false); 128 | expect(withOnExpire.options.onExpire).toHaveBeenCalledTimes(2); 129 | }); 130 | 131 | it('notifies of cache change when expiration re-established if onCacheChange', async () => { 132 | const onExpire = jest 133 | .fn() 134 | .mockReturnValueOnce(false) 135 | .mockReturnValue(true); 136 | 137 | const withOnExpire = moize.maxAge(1000, onExpire)(method, { 138 | onCacheChange: jest.fn(), 139 | }); 140 | 141 | withOnExpire(foo, bar); 142 | 143 | expect(withOnExpire.has([foo, bar])).toBe(true); 144 | expect(withOnExpire.options.onExpire).not.toHaveBeenCalled(); 145 | 146 | await new Promise((resolve) => setTimeout(resolve, 1100)); 147 | 148 | expect(withOnExpire.has([foo, bar])).toBe(true); 149 | expect(withOnExpire.options.onExpire).toHaveBeenCalledTimes(1); 150 | 151 | await new Promise((resolve) => setTimeout(resolve, 1100)); 152 | 153 | expect(withOnExpire.has([foo, bar])).toBe(false); 154 | expect(withOnExpire.options.onExpire).toHaveBeenCalledTimes(2); 155 | 156 | expect(withOnExpire.options.onCacheChange).toHaveBeenCalledWith( 157 | withOnExpire.cache, 158 | withOnExpire.options, 159 | withOnExpire 160 | ); 161 | }); 162 | }); 163 | -------------------------------------------------------------------------------- /__tests__/maxArgs.ts: -------------------------------------------------------------------------------- 1 | import moize from '../src'; 2 | 3 | const method = jest.fn(function (one: string, two: string) { 4 | return { one, two }; 5 | }); 6 | 7 | const foo = 'foo'; 8 | const bar = 'bar'; 9 | const baz = 'baz'; 10 | const qux = 'qux'; 11 | const quz = 'quz'; 12 | 13 | describe('moize.maxArgs', () => { 14 | afterEach(jest.clearAllMocks); 15 | 16 | [1, 2, 3, 4].forEach((limit) => { 17 | it(`limits the args to ${limit}`, () => { 18 | const memoized = moize.maxArgs(limit)(method); 19 | 20 | const args = [foo, bar, baz, qux, quz]; 21 | const limitedArgs = args.slice(0, limit); 22 | 23 | const resultA = memoized.apply(null, args); 24 | const resultB = memoized.apply(null, limitedArgs); 25 | 26 | expect(resultA).toEqual({ one: foo, two: bar }); 27 | expect(resultB).toBe(resultA); 28 | 29 | expect(method).toHaveBeenCalledTimes(1); 30 | }); 31 | }); 32 | 33 | it('will always return from cache if 0', () => { 34 | const memoized = moize.maxArgs(0)(method); 35 | 36 | const result = memoized(foo, bar); 37 | 38 | expect(result).toEqual({ one: foo, two: bar }); 39 | 40 | // @ts-ignore - allow bunk 41 | memoized(baz); 42 | // @ts-ignore - allow bunk 43 | memoized(123); 44 | // @ts-ignore - allow bunk 45 | memoized({}); 46 | // @ts-ignore - allow bunk 47 | memoized(); 48 | 49 | expect(method).toHaveBeenCalledTimes(1); 50 | }); 51 | 52 | it('will use the args passed if less than the size limited', () => { 53 | const memoized = moize.maxArgs(10)(method); 54 | 55 | const args = [foo, bar, baz, qux, quz]; 56 | 57 | const resultA = memoized.apply(null, args); 58 | const resultB = memoized.apply(null, [foo, bar, baz, qux, 'nope']); 59 | 60 | expect(resultA).toEqual({ one: foo, two: bar }); 61 | expect(resultB).not.toBe(resultA); 62 | 63 | expect(method).toHaveBeenCalledTimes(2); 64 | }); 65 | }); 66 | -------------------------------------------------------------------------------- /__tests__/promise.ts: -------------------------------------------------------------------------------- 1 | import Bluebird from 'bluebird'; 2 | import moize from '../src'; 3 | import { type Moizeable, type Moized } from '../index.d'; 4 | 5 | function createMethod( 6 | type: string, 7 | method: 'resolve' | 'reject', 8 | PromiseLibrary: PromiseConstructor 9 | ) { 10 | if (method === 'reject') { 11 | return function (number: number, otherNumber: number) { 12 | return PromiseLibrary.reject( 13 | new Error(`rejected ${number * otherNumber}`) 14 | ); 15 | }; 16 | } 17 | 18 | return function (number: number, otherNumber: number) { 19 | return PromiseLibrary.resolve(number * otherNumber); 20 | }; 21 | } 22 | 23 | const bluebirdMemoizedResolve = moize.promise( 24 | createMethod( 25 | 'bluebird', 26 | 'resolve', 27 | Bluebird as unknown as PromiseConstructor 28 | ), 29 | { profileName: 'bluebird (reject)' } 30 | ); 31 | const bluebirdMemoizedReject = moize.promise( 32 | createMethod( 33 | 'bluebird', 34 | 'reject', 35 | Bluebird as unknown as PromiseConstructor 36 | ), 37 | { profileName: 'bluebird (reject)' } 38 | ); 39 | const bluebirdMemoizedExpiring = moize.promise( 40 | createMethod( 41 | 'native', 42 | 'resolve', 43 | Bluebird as unknown as PromiseConstructor 44 | ), 45 | { 46 | maxAge: 1500, 47 | onCacheHit: jest.fn(), 48 | onExpire: jest.fn(), 49 | profileName: 'bluebird (expiring)', 50 | } 51 | ); 52 | 53 | const nativeMemoizedResolve = moize.promise( 54 | createMethod('native', 'resolve', Promise), 55 | { 56 | profileName: 'native', 57 | } 58 | ); 59 | const nativeMemoizedReject = moize.promise( 60 | createMethod('native', 'reject', Promise), 61 | { 62 | profileName: 'native (reject)', 63 | } 64 | ); 65 | const nativeMemoizedExpiring = moize.promise( 66 | createMethod('native', 'resolve', Promise), 67 | { 68 | maxAge: 1500, 69 | onCacheHit: jest.fn(), 70 | onExpire: jest.fn(), 71 | profileName: 'native (expiring)', 72 | } 73 | ); 74 | 75 | function testItem( 76 | key: number[], 77 | method: Moized, 78 | Constructor: any 79 | ) { 80 | const [number, otherNumber] = key; 81 | 82 | return method(number, otherNumber).then((result: number) => { 83 | expect(method.get(key)).toBeInstanceOf(Constructor); 84 | expect(method.get(key.slice().reverse())).toBe(undefined); 85 | 86 | expect(result).toEqual(number * otherNumber); 87 | }); 88 | } 89 | 90 | const TYPES = { 91 | bluebird: Bluebird, 92 | native: Promise, 93 | }; 94 | 95 | const METHODS = { 96 | bluebird: { 97 | resolve: bluebirdMemoizedResolve, 98 | reject: bluebirdMemoizedReject, 99 | expiring: bluebirdMemoizedExpiring, 100 | }, 101 | native: { 102 | resolve: nativeMemoizedResolve, 103 | reject: nativeMemoizedReject, 104 | expiring: nativeMemoizedExpiring, 105 | }, 106 | }; 107 | 108 | describe('moize.promise', () => { 109 | ['native', 'bluebird'].forEach((type) => { 110 | const Constructor = TYPES[type as keyof typeof TYPES]; 111 | 112 | ['resolve', 'reject', 'expiring'].forEach((test) => { 113 | const methodType = METHODS[type as keyof typeof METHODS]; 114 | const method = methodType[test as keyof typeof methodType]; 115 | 116 | it(`should handle ${test}`, async () => { 117 | try { 118 | await testItem([6, 9], method, Constructor); 119 | 120 | if (test === 'reject') { 121 | throw new Error(`${test} should have rejected`); 122 | } 123 | } catch (error) { 124 | if (test !== 'reject') { 125 | throw error; 126 | } 127 | } 128 | 129 | if (test === 'expiring') { 130 | expect(method.options.onCacheHit).toHaveBeenCalledWith( 131 | method.cache, 132 | method.options, 133 | method 134 | ); 135 | 136 | await new Promise((resolve) => 137 | setTimeout(resolve, method.options.maxAge * 2) 138 | ).then(() => { 139 | expect(method.options.onExpire).toHaveBeenCalledTimes( 140 | 1 141 | ); 142 | }); 143 | } 144 | }); 145 | }); 146 | }); 147 | }); 148 | -------------------------------------------------------------------------------- /__tests__/react.tsx: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React from 'react'; 3 | import ReactDOM from 'react-dom'; 4 | import moize from '../src'; 5 | import { copyStaticProperties } from '../src/instance'; 6 | 7 | describe('moize.react', () => { 8 | type ValueBarProps = { 9 | bar?: string; 10 | fn: (...args: any[]) => any; 11 | key?: string; 12 | object?: Record; 13 | value?: any; 14 | }; 15 | 16 | function _ValueBar({ bar, value }: ValueBarProps) { 17 | return ( 18 |
19 | {value} {bar} 20 |
21 | ); 22 | } 23 | 24 | _ValueBar.propTypes = { 25 | bar: PropTypes.string.isRequired, 26 | fn: PropTypes.func.isRequired, 27 | object: PropTypes.object.isRequired, 28 | value: PropTypes.string.isRequired, 29 | }; 30 | 31 | _ValueBar.defaultProps = { 32 | bar: 'default', 33 | }; 34 | 35 | const ValueBar = jest.fn(_ValueBar) as ( 36 | props: ValueBarProps 37 | ) => JSX.Element; 38 | 39 | // force static properties to be passed to mock 40 | copyStaticProperties(_ValueBar, ValueBar); 41 | 42 | const Memoized = moize.react(ValueBar); 43 | 44 | it('should have the correct static values', () => { 45 | expect(Memoized.propTypes).toBe(_ValueBar.propTypes); 46 | expect(Memoized.defaultProps).toBe(_ValueBar.defaultProps); 47 | expect(Memoized.displayName).toBe(`Moized(${ValueBar.name})`); 48 | }); 49 | 50 | it('should memoize the component renders', () => { 51 | type Props = { id: string; unused?: boolean }; 52 | 53 | const Component = ({ id }: Props) =>
; 54 | const ComponentSpy = jest.fn(Component) as typeof Component; 55 | const MoizedComponent = moize.react(ComponentSpy); 56 | const App = ({ id, unused }: Props) => ( 57 | 58 | ); 59 | 60 | const app = document.createElement('div'); 61 | 62 | document.body.appendChild(app); 63 | 64 | new Array(100).fill('id').forEach((id, index) => { 65 | ReactDOM.render(, app); 66 | }); 67 | 68 | // The number of calls is 3 because cache breaks twice, when `unused` prop is toggled. 69 | expect(ComponentSpy).toHaveBeenCalledTimes(3); 70 | }); 71 | 72 | it('should memoize the component renders with custom options', () => { 73 | type Props = { id: string; unused?: boolean }; 74 | 75 | const Component = ({ id }: Props) =>
; 76 | const ComponentSpy = jest.fn(Component) as typeof Component; 77 | const MoizedComponent = moize.react(ComponentSpy, { maxSize: 2 }); 78 | const App = ({ id, unused }: Props) => ( 79 | 80 | ); 81 | 82 | const app = document.createElement('div'); 83 | 84 | document.body.appendChild(app); 85 | 86 | new Array(100).fill('id').forEach((id, index) => { 87 | ReactDOM.render(, app); 88 | }); 89 | 90 | // The number of calls is 2 because both `unused` values are stored in cache. 91 | expect(ComponentSpy).toHaveBeenCalledTimes(2); 92 | }); 93 | 94 | it('should memoize the component renders including legacy context', () => { 95 | type Props = { id: string; unused?: boolean }; 96 | 97 | const Component = ({ id }: Props) =>
; 98 | const ComponentSpy = jest.fn( 99 | Component 100 | ) as unknown as typeof Component & { 101 | contextTypes: Record; 102 | }; 103 | 104 | ComponentSpy.contextTypes = { unused: PropTypes.bool.isRequired }; 105 | 106 | const MoizedComponent = moize.react(ComponentSpy); 107 | 108 | class App extends React.Component { 109 | static childContextTypes = { 110 | unused: PropTypes.bool.isRequired, 111 | }; 112 | 113 | getChildContext() { 114 | return { 115 | unused: this.props.unused, 116 | }; 117 | } 118 | 119 | render() { 120 | return ; 121 | } 122 | } 123 | 124 | const app = document.createElement('div'); 125 | 126 | document.body.appendChild(app); 127 | 128 | new Array(100).fill('id').forEach((id, index) => { 129 | ReactDOM.render(, app); 130 | }); 131 | 132 | // The number of calls is 3 because cache breaks twice, when `unused` context value is toggled. 133 | expect(ComponentSpy).toHaveBeenCalledTimes(3); 134 | }); 135 | 136 | it('should memoize on a per-instance basis on render', async () => { 137 | const foo = 'foo'; 138 | const bar = 'bar'; 139 | const baz = 'baz'; 140 | 141 | const data = [ 142 | { 143 | fn() { 144 | return foo; 145 | }, 146 | object: { value: foo }, 147 | value: foo, 148 | }, 149 | { 150 | bar, 151 | fn() { 152 | return bar; 153 | }, 154 | object: { value: bar }, 155 | value: bar, 156 | }, 157 | { 158 | fn() { 159 | return baz; 160 | }, 161 | object: { value: baz }, 162 | value: baz, 163 | }, 164 | ]; 165 | 166 | class App extends React.Component<{ isRerender?: boolean }> { 167 | MoizedComponent: typeof Memoized; 168 | 169 | componentDidMount() { 170 | expect(ValueBar).toHaveBeenCalledTimes(3); 171 | } 172 | 173 | componentDidUpdate() { 174 | // only one component rerendered based on dynamic props 175 | expect(ValueBar).toHaveBeenCalledTimes(4); 176 | } 177 | 178 | setMoizedComponent = (Ref: { 179 | MoizedComponent: typeof Memoized; 180 | }) => { 181 | this.MoizedComponent = Ref.MoizedComponent; 182 | }; 183 | 184 | render() { 185 | const { isRerender } = this.props; 186 | 187 | return ( 188 |
189 |

App

190 | 191 |
192 |

Memoized data list

193 | 194 | {data.map((values, index) => ( 195 | 201 | ))} 202 |
203 |
204 | ); 205 | } 206 | } 207 | 208 | function renderApp( 209 | isRerender?: boolean, 210 | onRender?: (value?: unknown) => void 211 | ) { 212 | ReactDOM.render(, app, onRender); 213 | } 214 | 215 | const app = document.createElement('div'); 216 | 217 | document.body.appendChild(app); 218 | 219 | renderApp(); 220 | 221 | expect(ValueBar).toHaveBeenCalledTimes(data.length); 222 | 223 | await new Promise((resolve) => 224 | setTimeout(() => { 225 | renderApp(true, resolve); 226 | }, 1000) 227 | ); 228 | 229 | expect(ValueBar).toHaveBeenCalledTimes(data.length + 1); 230 | }); 231 | 232 | it('should allow use of hooks', async () => { 233 | const timing = 1000; 234 | const app = document.createElement('div'); 235 | 236 | document.body.appendChild(app); 237 | 238 | const spy = jest.fn(); 239 | const TestComponent = moize.react(() => { 240 | const [txt, setTxt] = React.useState(0); 241 | 242 | React.useEffect(() => { 243 | setTimeout(() => { 244 | setTxt(Date.now()); 245 | spy(); 246 | }, timing); 247 | }, []); 248 | 249 | return {txt}; 250 | }); 251 | 252 | ReactDOM.render(, app); 253 | 254 | expect(spy).not.toHaveBeenCalled(); 255 | 256 | await new Promise((resolve) => setTimeout(resolve, timing + 200)); 257 | 258 | expect(spy).toHaveBeenCalled(); 259 | }); 260 | 261 | describe('edge cases', () => { 262 | it('should retain the original function name', () => { 263 | function MyComponent(): null { 264 | return null; 265 | } 266 | 267 | const memoized = moize.react(MyComponent); 268 | 269 | expect(memoized.name).toBe('moized(MyComponent)'); 270 | }); 271 | }); 272 | }); 273 | -------------------------------------------------------------------------------- /__tests__/serialize.ts: -------------------------------------------------------------------------------- 1 | import cloneDeep from 'lodash/cloneDeep'; 2 | import moize from '../src'; 3 | 4 | type Arg = { 5 | one: number; 6 | two: number; 7 | three: () => void; 8 | four: symbol; 9 | five: null; 10 | }; 11 | 12 | const method = jest.fn(function ({ one, two, three, four, five }: Arg) { 13 | return [one, two, three, four, five]; 14 | }); 15 | 16 | const memoized = moize.serialize(method); 17 | 18 | describe('moize.serialize', () => { 19 | afterEach(jest.clearAllMocks); 20 | 21 | it('serializes the args passed by', () => { 22 | const three = function () {}; 23 | const four = Symbol('foo'); 24 | 25 | const resultA = memoized({ one: 1, two: 2, three, four, five: null }); 26 | const resultB = memoized({ 27 | one: 1, 28 | two: 2, 29 | three() {}, 30 | four: Symbol('foo'), 31 | five: null, 32 | }); 33 | 34 | expect(resultA).toEqual([1, 2, three, four, null]); 35 | expect(resultB).toBe(resultA); 36 | 37 | expect(method).toHaveBeenCalledTimes(1); 38 | }); 39 | 40 | it('handles circular objects', () => { 41 | type Arg = { 42 | deeply: { 43 | nested: { 44 | circular: Arg | {}; 45 | }; 46 | }; 47 | }; 48 | 49 | const circularMethod = jest.fn((arg: Arg) => arg); 50 | const circularMemoized = moize.serialize(circularMethod); 51 | 52 | const circular: Arg = { 53 | deeply: { 54 | nested: { 55 | circular: {}, 56 | }, 57 | }, 58 | }; 59 | 60 | circular.deeply.nested.circular = circular; 61 | 62 | const resultA = circularMemoized(cloneDeep(circular)); 63 | const resultB = circularMemoized(cloneDeep(circular)); 64 | 65 | expect(resultB).toBe(resultA); 66 | 67 | expect(circularMethod).toHaveBeenCalledTimes(1); 68 | 69 | expect(circularMemoized.cache.keys).toEqual([ 70 | ['|{"deeply":{"nested":{"circular":"[ref=.]"}}}|'], 71 | ]); 72 | }); 73 | }); 74 | 75 | describe('moize.serializeWith', () => { 76 | afterEach(jest.clearAllMocks); 77 | 78 | it('serializes the arguments passed with the custom serializer', () => { 79 | const withSerializer = moize.serializeWith((args: any[]) => [ 80 | JSON.stringify(args), 81 | ])(method); 82 | 83 | const three = function () {}; 84 | const four = Symbol('foo'); 85 | 86 | const resultA = withSerializer({ 87 | one: 1, 88 | two: 2, 89 | three, 90 | four, 91 | five: null, 92 | }); 93 | const resultB = withSerializer({ 94 | one: 1, 95 | two: 2, 96 | three() {}, 97 | four: Symbol('foo'), 98 | five: null, 99 | }); 100 | 101 | expect(resultA).toEqual([1, 2, three, four, null]); 102 | expect(resultB).toBe(resultA); 103 | 104 | expect(method).toHaveBeenCalledTimes(1); 105 | }); 106 | }); 107 | -------------------------------------------------------------------------------- /__tests__/shallowEqual.ts: -------------------------------------------------------------------------------- 1 | import moize from '../src'; 2 | 3 | type Arg = { 4 | one: number; 5 | two: { 6 | deep: number; 7 | }; 8 | }; 9 | 10 | const method = jest.fn(function ({ one, two }: Arg) { 11 | return [one, two.deep]; 12 | }); 13 | 14 | const memoized = moize.shallow(method); 15 | 16 | describe('moize.shallow', () => { 17 | it('should memoized based on the shallow values', () => { 18 | const two = { deep: 2 }; 19 | 20 | const resultA = memoized({ one: 1, two }); 21 | const resultB = memoized({ one: 1, two }); 22 | 23 | expect(resultA).toEqual([1, 2]); 24 | expect(resultA).toBe(resultB); 25 | 26 | expect(method).toHaveBeenCalledTimes(1); 27 | 28 | const resultC = memoized({ one: 1, two: { ...two } }); 29 | 30 | expect(resultC).toEqual(resultA); 31 | expect(resultC).not.toBe(resultA); 32 | 33 | expect(method).toHaveBeenCalledTimes(2); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /__tests__/stats.ts: -------------------------------------------------------------------------------- 1 | import moize from '../src'; 2 | 3 | const foo = 'foo'; 4 | const bar = 'bar'; 5 | 6 | const method = jest.fn(function (one: string, two: string) { 7 | return { one, two }; 8 | }); 9 | 10 | describe('moize.isCollectingStats', () => { 11 | it('should identify if stats are being collected', () => { 12 | expect(moize.isCollectingStats()).toBe(false); 13 | 14 | moize.collectStats(); 15 | 16 | expect(moize.isCollectingStats()).toBe(true); 17 | 18 | moize.collectStats(false); 19 | 20 | expect(moize.isCollectingStats()).toBe(false); 21 | }); 22 | }); 23 | 24 | describe('moize.profile', () => { 25 | beforeEach(() => { 26 | moize.collectStats(); 27 | }); 28 | 29 | afterEach(() => { 30 | moize.collectStats(false); 31 | moize.clearStats(); 32 | }); 33 | 34 | it('should create a memoized method with the profileName passed', () => { 35 | const profileName = 'profileName'; 36 | const profiled = moize.profile(profileName)(method); 37 | 38 | profiled(foo, bar); 39 | profiled(foo, bar); 40 | 41 | expect(profiled.getStats()).toEqual({ 42 | calls: 2, 43 | hits: 1, 44 | usage: '50.0000%', 45 | }); 46 | 47 | profiled.clearStats(); 48 | 49 | expect(profiled.getStats()).toEqual({ 50 | calls: 0, 51 | hits: 0, 52 | usage: '0.0000%', 53 | }); 54 | }); 55 | 56 | it('should handle collecting more stats after clearing', () => { 57 | const profileName = 'profileName'; 58 | const profiled = moize.profile(profileName)(method); 59 | 60 | profiled(foo, bar); 61 | profiled(foo, bar); 62 | 63 | expect(profiled.getStats()).toEqual({ 64 | calls: 2, 65 | hits: 1, 66 | usage: '50.0000%', 67 | }); 68 | 69 | profiled.clearStats(); 70 | 71 | expect(profiled.getStats()).toEqual({ 72 | calls: 0, 73 | hits: 0, 74 | usage: '0.0000%', 75 | }); 76 | 77 | profiled(foo, bar); 78 | profiled(foo, bar); 79 | 80 | expect(profiled.getStats()).toEqual({ 81 | calls: 2, 82 | hits: 2, 83 | usage: '100.0000%', 84 | }); 85 | }); 86 | 87 | it('should profile a fallback name if one is not provided', () => { 88 | const originalError = global.Error; 89 | 90 | // @ts-ignore - dummy override 91 | global.Error = function () { 92 | return {}; 93 | }; 94 | 95 | const memoized = moize(method); 96 | 97 | memoized(foo, bar); 98 | memoized(foo, bar); 99 | 100 | expect(moize.getStats()).toEqual({ 101 | calls: 2, 102 | hits: 1, 103 | profiles: { 104 | [memoized.options.profileName]: { 105 | calls: 2, 106 | hits: 1, 107 | usage: '50.0000%', 108 | }, 109 | }, 110 | usage: '50.0000%', 111 | }); 112 | 113 | global.Error = originalError; 114 | }); 115 | }); 116 | 117 | describe('moize.getStats', () => { 118 | beforeEach(() => { 119 | moize.collectStats(); 120 | }); 121 | 122 | afterEach(() => { 123 | moize.collectStats(false); 124 | moize.clearStats(); 125 | }); 126 | 127 | it('should handle stats for all usages', () => { 128 | const profileName = 'profileName'; 129 | const profiled = moize.profile(profileName)(method); 130 | 131 | profiled(foo, bar); 132 | profiled(foo, bar); 133 | 134 | // specific stats 135 | expect(moize.getStats(profileName)).toEqual({ 136 | calls: 2, 137 | hits: 1, 138 | usage: '50.0000%', 139 | }); 140 | 141 | // global stats 142 | expect(moize.getStats()).toEqual({ 143 | calls: 2, 144 | hits: 1, 145 | profiles: { 146 | [profileName]: { 147 | calls: 2, 148 | hits: 1, 149 | usage: '50.0000%', 150 | }, 151 | }, 152 | usage: '50.0000%', 153 | }); 154 | 155 | moize.clearStats(); 156 | 157 | expect(moize.getStats()).toEqual({ 158 | calls: 0, 159 | hits: 0, 160 | profiles: {}, 161 | usage: '0.0000%', 162 | }); 163 | }); 164 | 165 | it('should warn when getting stats and stats are not being collected', () => { 166 | moize.collectStats(false); 167 | 168 | const warn = jest.spyOn(console, 'warn'); 169 | 170 | moize.getStats(); 171 | 172 | expect(warn).toHaveBeenCalledWith( 173 | 'Stats are not currently being collected, please run "collectStats" to enable them.' 174 | ); 175 | 176 | warn.mockRestore(); 177 | }); 178 | }); 179 | -------------------------------------------------------------------------------- /__tests__/transformArgs.ts: -------------------------------------------------------------------------------- 1 | import moize from '../src'; 2 | 3 | const method = jest.fn(function (one: string, two: string, three: string) { 4 | return { one, two, three }; 5 | }); 6 | 7 | function transformer(args: string[]) { 8 | const newKey: string[] = []; 9 | 10 | let index = args.length; 11 | 12 | while (--index) { 13 | newKey[index - 1] = args[index]; 14 | } 15 | 16 | return newKey; 17 | } 18 | 19 | const memoized = moize.transformArgs(transformer)(method); 20 | 21 | const foo = 'foo'; 22 | const bar = 'bar'; 23 | const baz = 'baz'; 24 | 25 | describe('moize.transformArgs', () => { 26 | it('limits the args memoized by', () => { 27 | const resultA = memoized(foo, bar, baz); 28 | const resultB = memoized(null, bar, baz); 29 | 30 | expect(resultA).toEqual({ one: foo, two: bar, three: baz }); 31 | expect(resultB).toBe(resultA); 32 | 33 | expect(method).toHaveBeenCalledTimes(1); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /__tests__/updateCacheForKey.ts: -------------------------------------------------------------------------------- 1 | import moize from '../src'; 2 | 3 | type Type = { 4 | number: number; 5 | }; 6 | 7 | const method = (one: number, two: Type) => one + two.number; 8 | const promiseMethodResolves = (one: number, two: Type) => 9 | new Promise((resolve) => setTimeout(() => resolve(one + two.number), 1000)); 10 | const promiseMethodRejects = 11 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 12 | (one: number, two: Type) => 13 | new Promise((resolve, reject) => 14 | setTimeout(() => reject(new Error('boom')), 1000) 15 | ); 16 | 17 | describe('moize.updateCacheForKey', () => { 18 | describe('success', () => { 19 | it('will refresh the cache', () => { 20 | const moized = moize.maxSize(2)(method, { 21 | updateCacheForKey(args) { 22 | return args[1].number % 2 === 0; 23 | }, 24 | }); 25 | 26 | const mutated = { number: 5 }; 27 | 28 | const result = moized(6, mutated); 29 | 30 | expect(result).toBe(11); 31 | 32 | mutated.number = 11; 33 | 34 | const mutatedResult = moized(6, mutated); 35 | 36 | // Result was not recalculated because `updateCacheForKey` returned `false` and the values are 37 | // seen as unchanged. 38 | expect(mutatedResult).toBe(result); 39 | 40 | mutated.number = 10; 41 | 42 | const refreshedResult = moized(6, mutated); 43 | 44 | // Result was recalculated because `updateCacheForKey` returned `true`. 45 | expect(refreshedResult).not.toBe(result); 46 | expect(refreshedResult).toBe(16); 47 | 48 | const { keys, values } = moized.cacheSnapshot; 49 | 50 | expect(keys).toEqual([[6, mutated]]); 51 | expect(values).toEqual([16]); 52 | }); 53 | 54 | it('will refresh the cache based on external values', async () => { 55 | const mockMethod = jest.fn(method); 56 | 57 | let lastUpdate = Date.now(); 58 | 59 | const moized = moize.maxSize(2)(mockMethod, { 60 | updateCacheForKey() { 61 | const now = Date.now(); 62 | const last = lastUpdate; 63 | 64 | lastUpdate = now; 65 | 66 | return last + 1000 < now; 67 | }, 68 | }); 69 | 70 | const mutated = { number: 5 }; 71 | 72 | moized(6, mutated); 73 | moized(6, mutated); 74 | moized(6, mutated); 75 | 76 | expect(mockMethod).toHaveBeenCalledTimes(1); 77 | 78 | await new Promise((resolve) => setTimeout(resolve, 2000)); 79 | 80 | moized(6, mutated); 81 | 82 | expect(mockMethod).toHaveBeenCalledTimes(2); 83 | }); 84 | 85 | it('will refresh the cache when used with promises', async () => { 86 | const moized = moize.maxSize(2)(promiseMethodResolves, { 87 | isPromise: true, 88 | updateCacheForKey(args) { 89 | return args[1].number % 2 === 0; 90 | }, 91 | }); 92 | 93 | const mutated = { number: 5 }; 94 | 95 | const result = await moized(6, mutated); 96 | 97 | expect(result).toBe(11); 98 | 99 | mutated.number = 11; 100 | 101 | const mutatedResult = await moized(6, mutated); 102 | 103 | // Result was not recalculated because `updateCacheForKey` returned `false` and the values are 104 | // seen as unchanged. 105 | expect(mutatedResult).toBe(result); 106 | 107 | mutated.number = 10; 108 | 109 | const refreshedResult = await moized(6, mutated); 110 | 111 | // Result was recalculated because `updateCacheForKey` returned `true`. 112 | expect(refreshedResult).not.toBe(result); 113 | expect(refreshedResult).toBe(16); 114 | 115 | const { keys, values } = moized.cacheSnapshot; 116 | 117 | expect(keys).toEqual([[6, mutated]]); 118 | expect(values).toEqual([Promise.resolve(16)]); 119 | }); 120 | 121 | it('will refresh the cache when used with custom key transformers', () => { 122 | type ConditionalIncrement = { 123 | force?: boolean; 124 | }; 125 | 126 | let count = 0; 127 | 128 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 129 | const increment = (_?: ConditionalIncrement) => ++count; 130 | 131 | const moized = moize.maxSize(2)(increment, { 132 | isSerialized: true, 133 | updateCacheForKey: (args: [ConditionalIncrement]) => 134 | args[0] && args[0].force === true, 135 | serializer: () => ['always same'], 136 | }); 137 | 138 | expect(moized()).toBe(1); 139 | expect(moized()).toBe(1); 140 | expect(moized({ force: true })).toBe(2); 141 | expect(moized()).toBe(2); 142 | }); 143 | 144 | it('will refresh the cache with shorthand', () => { 145 | const moized = moize.updateCacheForKey( 146 | (args) => args[1].number % 2 === 0 147 | )(method); 148 | 149 | const mutated = { number: 5 }; 150 | 151 | const result = moized(6, mutated); 152 | 153 | expect(result).toBe(11); 154 | 155 | mutated.number = 11; 156 | 157 | const mutatedResult = moized(6, mutated); 158 | 159 | // Result was not recalculated because `updateCacheForKey` returned `false` and the values are 160 | // seen as unchanged. 161 | expect(mutatedResult).toBe(result); 162 | 163 | mutated.number = 10; 164 | 165 | const refreshedResult = moized(6, mutated); 166 | 167 | // Result was recalculated because `updateCacheForKey` returned `true`. 168 | expect(refreshedResult).not.toBe(result); 169 | expect(refreshedResult).toBe(16); 170 | 171 | const { keys, values } = moized.cacheSnapshot; 172 | 173 | expect(keys).toEqual([[6, mutated]]); 174 | expect(values).toEqual([16]); 175 | }); 176 | 177 | it('will refresh the cache with composed shorthand', () => { 178 | const moizer = moize.compose( 179 | moize.maxSize(2), 180 | moize.updateCacheForKey((args) => args[1].number % 2 === 0) 181 | ); 182 | const moized = moizer(method); 183 | 184 | const mutated = { number: 5 }; 185 | 186 | const result = moized(6, mutated); 187 | 188 | expect(result).toBe(11); 189 | 190 | mutated.number = 11; 191 | 192 | const mutatedResult = moized(6, mutated); 193 | 194 | // Result was not recalculated because `updateCacheForKey` returned `false` and the values are 195 | // seen as unchanged. 196 | expect(mutatedResult).toBe(result); 197 | 198 | mutated.number = 10; 199 | 200 | const refreshedResult = moized(6, mutated); 201 | 202 | // Result was recalculated because `updateCacheForKey` returned `true`. 203 | expect(refreshedResult).not.toBe(result); 204 | expect(refreshedResult).toBe(16); 205 | 206 | const { keys, values } = moized.cacheSnapshot; 207 | 208 | expect(keys).toEqual([[6, mutated]]); 209 | expect(values).toEqual([16]); 210 | }); 211 | }); 212 | 213 | describe('fail', () => { 214 | it('surfaces the error if the function fails', () => { 215 | const moized = moize.maxSize(2)( 216 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 217 | (_1: number, _2: Type) => { 218 | throw new Error('boom'); 219 | }, 220 | { 221 | updateCacheForKey(args) { 222 | return args[1].number % 2 === 0; 223 | }, 224 | } 225 | ); 226 | 227 | const mutated = { number: 5 }; 228 | 229 | expect(() => moized(6, mutated)).toThrow(new Error('boom')); 230 | }); 231 | 232 | it('surfaces the error if the promise rejects', async () => { 233 | const moized = moize.maxSize(2)(promiseMethodRejects, { 234 | isPromise: true, 235 | updateCacheForKey(args) { 236 | return args[1].number % 2 === 0; 237 | }, 238 | }); 239 | 240 | const mutated = { number: 5 }; 241 | 242 | await expect(moized(6, mutated)).rejects.toEqual(new Error('boom')); 243 | }); 244 | 245 | it('should have nothing in cache if promise is rejected and key was never present', async () => { 246 | const moized = moize.maxSize(2)(promiseMethodRejects, { 247 | isPromise: true, 248 | updateCacheForKey(args) { 249 | return args[1].number % 2 === 0; 250 | }, 251 | }); 252 | 253 | const mutated = { number: 5 }; 254 | 255 | await expect(moized(6, mutated)).rejects.toEqual(new Error('boom')); 256 | 257 | expect(moized.keys()).toEqual([]); 258 | expect(moized.values()).toEqual([]); 259 | }); 260 | 261 | // For some reason, this is causing `jest` to crash instead of handle the rejection 262 | it.skip('should have nothing in cache if promise is rejected and key was present', async () => { 263 | const moized = moize.maxSize(2)(promiseMethodRejects, { 264 | isPromise: true, 265 | updateCacheForKey(args) { 266 | return args[1].number % 2 === 0; 267 | }, 268 | }); 269 | 270 | const mutated = { number: 5 }; 271 | 272 | moized.set([6, mutated], Promise.resolve(11)); 273 | 274 | expect(moized.get([6, mutated])).toEqual(Promise.resolve(11)); 275 | 276 | mutated.number = 10; 277 | 278 | await expect(moized(6, mutated)).rejects.toEqual(new Error('boom')); 279 | 280 | expect(moized.keys()).toEqual([]); 281 | expect(moized.values()).toEqual([]); 282 | }); 283 | }); 284 | 285 | describe('infrastructure', () => { 286 | it('should have all the static properties of a standard moized method', () => { 287 | const moized = moize.maxSize(2)(promiseMethodResolves, { 288 | updateCacheForKey(args) { 289 | return args[1].number % 2 === 0; 290 | }, 291 | }); 292 | const standardMoized = moize.maxSize(2)(promiseMethodResolves); 293 | 294 | expect(Object.getOwnPropertyNames(moized)).toEqual( 295 | Object.getOwnPropertyNames(standardMoized) 296 | ); 297 | }); 298 | }); 299 | 300 | describe('edge cases', () => { 301 | it('should retain the original function name', () => { 302 | function myNamedFunction() {} 303 | 304 | const memoized = moize(myNamedFunction, { 305 | updateCacheForKey: () => false, 306 | }); 307 | 308 | expect(memoized.name).toBe('moized(myNamedFunction)'); 309 | }); 310 | }); 311 | }); 312 | -------------------------------------------------------------------------------- /benchmark/Benchmark results.ods: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/planttheidea/moize/b04c9e4b8a4c935694b3cc4803d2720adc0b79d0/benchmark/Benchmark results.ods -------------------------------------------------------------------------------- /benchmark/addy-osmani.js: -------------------------------------------------------------------------------- 1 | /* 2 | * memoize.js 3 | * by @philogb and @addyosmani 4 | * with further optimizations by @mathias 5 | * and @DmitryBaranovsk 6 | * perf tests: http://bit.ly/q3zpG3 7 | * Released under an MIT license. 8 | */ 9 | module.exports = function memoize( fn ) { 10 | return function () { 11 | var args = Array.prototype.slice.call(arguments), 12 | hash = "", 13 | i = args.length; 14 | currentArg = null; 15 | while (i--) { 16 | currentArg = args[i]; 17 | hash += (currentArg === Object(currentArg)) ? 18 | JSON.stringify(currentArg) : currentArg; 19 | fn.memoize || (fn.memoize = {}); 20 | } 21 | return (hash in fn.memoize) ? fn.memoize[hash] : 22 | fn.memoize[hash] = fn.apply(this, args); 23 | }; 24 | } 25 | -------------------------------------------------------------------------------- /benchmark/benchmark_results.csv: -------------------------------------------------------------------------------- 1 | "Name","Overall (average)","Single (average)","Multiple (average)","single primitive","single array","single object","multiple primitive","multiple array","multiple object" 2 | "moize","71,177,801","98,393,482","43,962,121","139,808,786","97,571,202","57,800,460","44,509,528","44,526,039","42,850,796" 3 | "lru-memoize","48,391,839","64,270,849","32,512,830","77,863,436","59,876,764","55,072,348","29,917,027","33,308,028","34,313,435" 4 | "mem","42,348,320","83,158,473","1,538,166","128,731,510","73,473,478","47,270,433","2,012,120","1,565,253","1,037,126" 5 | "fast-memoize","33,145,713","64,942,152","1,349,274","190,677,799","2,149,467","1,999,192","1,718,229","1,297,911","1,031,683" 6 | "lodash","25,700,293","49,941,573","1,459,013","67,513,655","48,874,559","33,436,506","1,861,982","1,402,532","1,112,527" 7 | "memoizee","21,546,499","27,447,855","15,645,143","29,701,124","27,294,197","25,348,244","15,359,792","15,855,421","15,720,217" 8 | "ramda","18,804,380","35,919,033","1,689,727","101,557,928","1,895,956","4,303,215","2,305,025","1,597,131","1,167,025" 9 | "memoizerific","6,745,058","7,382,030","6,108,086","8,488,885","6,427,832","7,229,375","5,772,461","6,278,344","6,273,453" 10 | "underscore","6,701,695","11,698,265","1,705,126","18,249,423","4,695,658","12,149,714","2,310,412","1,630,769","1,174,197" 11 | "addy-osmani","4,926,732","6,370,152","3,483,311","12,506,809","3,568,399","3,035,249","6,898,542","2,009,089","1,542,304" -------------------------------------------------------------------------------- /docs/.nojekyll: -------------------------------------------------------------------------------- 1 | TypeDoc added this file to prevent GitHub Pages from using Jekyll. You can turn off this behavior by setting the `githubPages` option to false. -------------------------------------------------------------------------------- /docs/assets/highlight.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --light-hl-0: #001080; 3 | --dark-hl-0: #9CDCFE; 4 | --light-hl-1: #000000; 5 | --dark-hl-1: #D4D4D4; 6 | --light-hl-2: #AF00DB; 7 | --dark-hl-2: #C586C0; 8 | --light-hl-3: #A31515; 9 | --dark-hl-3: #CE9178; 10 | --light-hl-4: #0000FF; 11 | --dark-hl-4: #569CD6; 12 | --light-hl-5: #0070C1; 13 | --dark-hl-5: #4FC1FF; 14 | --light-hl-6: #795E26; 15 | --dark-hl-6: #DCDCAA; 16 | --light-hl-7: #267F99; 17 | --dark-hl-7: #4EC9B0; 18 | --light-hl-8: #098658; 19 | --dark-hl-8: #B5CEA8; 20 | --light-hl-9: #008000; 21 | --dark-hl-9: #6A9955; 22 | --light-hl-10: #800000; 23 | --dark-hl-10: #808080; 24 | --light-hl-11: #800000; 25 | --dark-hl-11: #569CD6; 26 | --light-hl-12: #000000FF; 27 | --dark-hl-12: #D4D4D4; 28 | --light-code-background: #FFFFFF; 29 | --dark-code-background: #1E1E1E; 30 | } 31 | 32 | @media (prefers-color-scheme: light) { :root { 33 | --hl-0: var(--light-hl-0); 34 | --hl-1: var(--light-hl-1); 35 | --hl-2: var(--light-hl-2); 36 | --hl-3: var(--light-hl-3); 37 | --hl-4: var(--light-hl-4); 38 | --hl-5: var(--light-hl-5); 39 | --hl-6: var(--light-hl-6); 40 | --hl-7: var(--light-hl-7); 41 | --hl-8: var(--light-hl-8); 42 | --hl-9: var(--light-hl-9); 43 | --hl-10: var(--light-hl-10); 44 | --hl-11: var(--light-hl-11); 45 | --hl-12: var(--light-hl-12); 46 | --code-background: var(--light-code-background); 47 | } } 48 | 49 | @media (prefers-color-scheme: dark) { :root { 50 | --hl-0: var(--dark-hl-0); 51 | --hl-1: var(--dark-hl-1); 52 | --hl-2: var(--dark-hl-2); 53 | --hl-3: var(--dark-hl-3); 54 | --hl-4: var(--dark-hl-4); 55 | --hl-5: var(--dark-hl-5); 56 | --hl-6: var(--dark-hl-6); 57 | --hl-7: var(--dark-hl-7); 58 | --hl-8: var(--dark-hl-8); 59 | --hl-9: var(--dark-hl-9); 60 | --hl-10: var(--dark-hl-10); 61 | --hl-11: var(--dark-hl-11); 62 | --hl-12: var(--dark-hl-12); 63 | --code-background: var(--dark-code-background); 64 | } } 65 | 66 | :root[data-theme='light'] { 67 | --hl-0: var(--light-hl-0); 68 | --hl-1: var(--light-hl-1); 69 | --hl-2: var(--light-hl-2); 70 | --hl-3: var(--light-hl-3); 71 | --hl-4: var(--light-hl-4); 72 | --hl-5: var(--light-hl-5); 73 | --hl-6: var(--light-hl-6); 74 | --hl-7: var(--light-hl-7); 75 | --hl-8: var(--light-hl-8); 76 | --hl-9: var(--light-hl-9); 77 | --hl-10: var(--light-hl-10); 78 | --hl-11: var(--light-hl-11); 79 | --hl-12: var(--light-hl-12); 80 | --code-background: var(--light-code-background); 81 | } 82 | 83 | :root[data-theme='dark'] { 84 | --hl-0: var(--dark-hl-0); 85 | --hl-1: var(--dark-hl-1); 86 | --hl-2: var(--dark-hl-2); 87 | --hl-3: var(--dark-hl-3); 88 | --hl-4: var(--dark-hl-4); 89 | --hl-5: var(--dark-hl-5); 90 | --hl-6: var(--dark-hl-6); 91 | --hl-7: var(--dark-hl-7); 92 | --hl-8: var(--dark-hl-8); 93 | --hl-9: var(--dark-hl-9); 94 | --hl-10: var(--dark-hl-10); 95 | --hl-11: var(--dark-hl-11); 96 | --hl-12: var(--dark-hl-12); 97 | --code-background: var(--dark-code-background); 98 | } 99 | 100 | .hl-0 { color: var(--hl-0); } 101 | .hl-1 { color: var(--hl-1); } 102 | .hl-2 { color: var(--hl-2); } 103 | .hl-3 { color: var(--hl-3); } 104 | .hl-4 { color: var(--hl-4); } 105 | .hl-5 { color: var(--hl-5); } 106 | .hl-6 { color: var(--hl-6); } 107 | .hl-7 { color: var(--hl-7); } 108 | .hl-8 { color: var(--hl-8); } 109 | .hl-9 { color: var(--hl-9); } 110 | .hl-10 { color: var(--hl-10); } 111 | .hl-11 { color: var(--hl-11); } 112 | .hl-12 { color: var(--hl-12); } 113 | pre, code { background: var(--code-background); } 114 | -------------------------------------------------------------------------------- /docs/assets/search.js: -------------------------------------------------------------------------------- 1 | window.searchData = JSON.parse("{\"rows\":[{\"kind\":64,\"name\":\"default\",\"url\":\"functions/default-1.html\",\"classes\":\"\"},{\"kind\":4,\"name\":\"default\",\"url\":\"modules/default.html\",\"classes\":\"\"},{\"kind\":32,\"name\":\"clearStats\",\"url\":\"variables/default.clearStats.html\",\"classes\":\"\",\"parent\":\"default\"},{\"kind\":65536,\"name\":\"__type\",\"url\":\"variables/default.clearStats.html#__type\",\"classes\":\"\",\"parent\":\"default.clearStats\"},{\"kind\":32,\"name\":\"collectStats\",\"url\":\"variables/default.collectStats.html\",\"classes\":\"\",\"parent\":\"default\"},{\"kind\":65536,\"name\":\"__type\",\"url\":\"variables/default.collectStats.html#__type\",\"classes\":\"\",\"parent\":\"default.collectStats\"},{\"kind\":64,\"name\":\"compose\",\"url\":\"functions/default.compose.html\",\"classes\":\"\",\"parent\":\"default\"},{\"kind\":32,\"name\":\"deep\",\"url\":\"variables/default.deep.html\",\"classes\":\"\",\"parent\":\"default\"},{\"kind\":32,\"name\":\"getStats\",\"url\":\"variables/default.getStats.html\",\"classes\":\"\",\"parent\":\"default\"},{\"kind\":65536,\"name\":\"__type\",\"url\":\"variables/default.getStats.html#__type\",\"classes\":\"\",\"parent\":\"default.getStats\"},{\"kind\":32,\"name\":\"infinite\",\"url\":\"variables/default.infinite.html\",\"classes\":\"\",\"parent\":\"default\"},{\"kind\":64,\"name\":\"isCollectingStats\",\"url\":\"functions/default.isCollectingStats.html\",\"classes\":\"\",\"parent\":\"default\"},{\"kind\":64,\"name\":\"isMoized\",\"url\":\"functions/default.isMoized.html\",\"classes\":\"\",\"parent\":\"default\"},{\"kind\":64,\"name\":\"matchesArg\",\"url\":\"functions/default.matchesArg.html\",\"classes\":\"\",\"parent\":\"default\"},{\"kind\":64,\"name\":\"matchesKey\",\"url\":\"functions/default.matchesKey.html\",\"classes\":\"\",\"parent\":\"default\"},{\"kind\":32,\"name\":\"maxAge\",\"url\":\"variables/default.maxAge.html\",\"classes\":\"\",\"parent\":\"default\"},{\"kind\":64,\"name\":\"maxArgs\",\"url\":\"functions/default.maxArgs.html\",\"classes\":\"\",\"parent\":\"default\"},{\"kind\":64,\"name\":\"maxSize\",\"url\":\"functions/default.maxSize.html\",\"classes\":\"\",\"parent\":\"default\"},{\"kind\":64,\"name\":\"profile\",\"url\":\"functions/default.profile.html\",\"classes\":\"\",\"parent\":\"default\"},{\"kind\":32,\"name\":\"promise\",\"url\":\"variables/default.promise.html\",\"classes\":\"\",\"parent\":\"default\"},{\"kind\":32,\"name\":\"react\",\"url\":\"variables/default.react.html\",\"classes\":\"\",\"parent\":\"default\"},{\"kind\":32,\"name\":\"serialize\",\"url\":\"variables/default.serialize.html\",\"classes\":\"\",\"parent\":\"default\"},{\"kind\":64,\"name\":\"serializeWith\",\"url\":\"functions/default.serializeWith.html\",\"classes\":\"\",\"parent\":\"default\"},{\"kind\":32,\"name\":\"shallow\",\"url\":\"variables/default.shallow.html\",\"classes\":\"\",\"parent\":\"default\"},{\"kind\":64,\"name\":\"transformArgs\",\"url\":\"functions/default.transformArgs.html\",\"classes\":\"\",\"parent\":\"default\"},{\"kind\":64,\"name\":\"updateCacheForKey\",\"url\":\"functions/default.updateCacheForKey.html\",\"classes\":\"\",\"parent\":\"default\"}],\"index\":{\"version\":\"2.3.9\",\"fields\":[\"name\",\"comment\"],\"fieldVectors\":[[\"name/0\",[0,23.795]],[\"comment/0\",[]],[\"name/1\",[0,23.795]],[\"comment/1\",[]],[\"name/2\",[1,28.904]],[\"comment/2\",[]],[\"name/3\",[2,20.431]],[\"comment/3\",[]],[\"name/4\",[3,28.904]],[\"comment/4\",[]],[\"name/5\",[2,20.431]],[\"comment/5\",[]],[\"name/6\",[4,28.904]],[\"comment/6\",[]],[\"name/7\",[5,28.904]],[\"comment/7\",[]],[\"name/8\",[6,28.904]],[\"comment/8\",[]],[\"name/9\",[2,20.431]],[\"comment/9\",[]],[\"name/10\",[7,28.904]],[\"comment/10\",[]],[\"name/11\",[8,28.904]],[\"comment/11\",[]],[\"name/12\",[9,28.904]],[\"comment/12\",[]],[\"name/13\",[10,28.904]],[\"comment/13\",[]],[\"name/14\",[11,28.904]],[\"comment/14\",[]],[\"name/15\",[12,28.904]],[\"comment/15\",[]],[\"name/16\",[13,28.904]],[\"comment/16\",[]],[\"name/17\",[14,28.904]],[\"comment/17\",[]],[\"name/18\",[15,28.904]],[\"comment/18\",[]],[\"name/19\",[16,28.904]],[\"comment/19\",[]],[\"name/20\",[17,28.904]],[\"comment/20\",[]],[\"name/21\",[18,28.904]],[\"comment/21\",[]],[\"name/22\",[19,28.904]],[\"comment/22\",[]],[\"name/23\",[20,28.904]],[\"comment/23\",[]],[\"name/24\",[21,28.904]],[\"comment/24\",[]],[\"name/25\",[22,28.904]],[\"comment/25\",[]]],\"invertedIndex\":[[\"__type\",{\"_index\":2,\"name\":{\"3\":{},\"5\":{},\"9\":{}},\"comment\":{}}],[\"clearstats\",{\"_index\":1,\"name\":{\"2\":{}},\"comment\":{}}],[\"collectstats\",{\"_index\":3,\"name\":{\"4\":{}},\"comment\":{}}],[\"compose\",{\"_index\":4,\"name\":{\"6\":{}},\"comment\":{}}],[\"deep\",{\"_index\":5,\"name\":{\"7\":{}},\"comment\":{}}],[\"default\",{\"_index\":0,\"name\":{\"0\":{},\"1\":{}},\"comment\":{}}],[\"getstats\",{\"_index\":6,\"name\":{\"8\":{}},\"comment\":{}}],[\"infinite\",{\"_index\":7,\"name\":{\"10\":{}},\"comment\":{}}],[\"iscollectingstats\",{\"_index\":8,\"name\":{\"11\":{}},\"comment\":{}}],[\"ismoized\",{\"_index\":9,\"name\":{\"12\":{}},\"comment\":{}}],[\"matchesarg\",{\"_index\":10,\"name\":{\"13\":{}},\"comment\":{}}],[\"matcheskey\",{\"_index\":11,\"name\":{\"14\":{}},\"comment\":{}}],[\"maxage\",{\"_index\":12,\"name\":{\"15\":{}},\"comment\":{}}],[\"maxargs\",{\"_index\":13,\"name\":{\"16\":{}},\"comment\":{}}],[\"maxsize\",{\"_index\":14,\"name\":{\"17\":{}},\"comment\":{}}],[\"profile\",{\"_index\":15,\"name\":{\"18\":{}},\"comment\":{}}],[\"promise\",{\"_index\":16,\"name\":{\"19\":{}},\"comment\":{}}],[\"react\",{\"_index\":17,\"name\":{\"20\":{}},\"comment\":{}}],[\"serialize\",{\"_index\":18,\"name\":{\"21\":{}},\"comment\":{}}],[\"serializewith\",{\"_index\":19,\"name\":{\"22\":{}},\"comment\":{}}],[\"shallow\",{\"_index\":20,\"name\":{\"23\":{}},\"comment\":{}}],[\"transformargs\",{\"_index\":21,\"name\":{\"24\":{}},\"comment\":{}}],[\"updatecacheforkey\",{\"_index\":22,\"name\":{\"25\":{}},\"comment\":{}}]],\"pipeline\":[]}}"); -------------------------------------------------------------------------------- /docs/modules.html: -------------------------------------------------------------------------------- 1 | moize
2 |
3 | 10 |
11 |
12 |
13 |
14 |

moize

15 |
16 |
17 |

Index

18 |
19 |

Namespaces

20 |
default 21 |
22 |
23 |

Functions

24 |
default 25 |
26 |
27 | 41 |
70 |
71 |

Generated using TypeDoc

72 |
-------------------------------------------------------------------------------- /docs/variables/default.deep.html: -------------------------------------------------------------------------------- 1 | deep | moize
2 |
3 | 10 |
11 |
12 |
13 |
14 | 18 |

Variable deep

19 |
deep: Moizer<{
    isDeepEqual: true;
}>
20 |
21 |

Function

22 |

Name

deep

23 | 24 |

Memberof

module:moize

25 | 26 |

Alias

moize.deep

27 | 28 |

Description

should deep equality check be used

29 | 30 |

Returns

the moizer function

31 |
34 |
35 | 49 |
78 |
79 |

Generated using TypeDoc

80 |
-------------------------------------------------------------------------------- /docs/variables/default.maxAge.html: -------------------------------------------------------------------------------- 1 | maxAge | moize
2 |
3 | 10 |
11 |
12 |
13 |
14 | 18 |

Variable maxAge

19 |
maxAge: MaxAge
20 |
21 |

Function

22 |

Name

maxAge

23 | 24 |

Memberof

module:moize

25 | 26 |

Alias

moize.maxAge

27 | 28 |

Description

a moized method where the age of the cache is limited to the number of milliseconds passed

29 | 30 |

Param

the TTL of the value in cache

31 | 32 |

Returns

the moizer function

33 |
36 |
37 | 51 |
80 |
81 |

Generated using TypeDoc

82 |
-------------------------------------------------------------------------------- /docs/variables/default.react.html: -------------------------------------------------------------------------------- 1 | react | moize
2 |
3 | 10 |
11 |
12 |
13 |
14 | 18 |

Variable react

19 |
react: Moizer<{
    isReact: true;
}>
20 |
21 |

Function

22 |

Name

react

23 | 24 |

Memberof

module:moize

25 | 26 |

Alias

moize.react

27 | 28 |

Description

a moized method specific to caching React element values

29 | 30 |

Returns

the moizer function

31 |
34 |
35 | 49 |
78 |
79 |

Generated using TypeDoc

80 |
-------------------------------------------------------------------------------- /docs/variables/default.shallow.html: -------------------------------------------------------------------------------- 1 | shallow | moize
2 |
3 | 10 |
11 |
12 |
13 |
14 | 18 |

Variable shallow

19 |
shallow: Moizer<{
    isShallowEqual: true;
}>
20 |
21 |

Function

22 |

Name

shallow

23 | 24 |

Memberof

module:moize

25 | 26 |

Alias

moize.shallow

27 | 28 |

Description

should shallow equality check be used

29 | 30 |

Returns

the moizer function

31 |
34 |
35 | 49 |
78 |
79 |

Generated using TypeDoc

80 |
-------------------------------------------------------------------------------- /es-to-mjs.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | 6 | const pkg = require('./package.json'); 7 | 8 | const BASE_PATH = __dirname; 9 | const SOURCE_ENTRY = path.join(BASE_PATH, pkg.module); 10 | const SOURCE_MAP = `${SOURCE_ENTRY}.map`; 11 | const SOURCE_TYPES = path.join(BASE_PATH, 'index.d.ts'); 12 | const DESTINATION = 'mjs'; 13 | const DESTINATION_ENTRY = path.join(BASE_PATH, DESTINATION, 'index.mjs'); 14 | const DESTINATION_MAP = `${DESTINATION_ENTRY}.map`; 15 | const DESTINATION_TYPES = path.join(BASE_PATH, DESTINATION, 'index.d.mts'); 16 | 17 | function getFileName(filename) { 18 | return filename.replace(`${BASE_PATH}/`, ''); 19 | } 20 | 21 | try { 22 | if (!fs.existsSync(path.join(__dirname, 'mjs'))) { 23 | fs.mkdirSync(path.join(__dirname, 'mjs')); 24 | } 25 | 26 | fs.copyFileSync(SOURCE_ENTRY, DESTINATION_ENTRY); 27 | 28 | const contents = fs 29 | .readFileSync(DESTINATION_ENTRY, { encoding: 'utf8' }) 30 | .replace('fast-equals', 'fast-equals/dist/fast-equals.mjs') 31 | .replace('fast-stringify', 'fast-stringify/mjs/index.mjs') 32 | .replace('micro-memoize', 'micro-memoize/mjs/index.mjs') 33 | .replace(/\/\/# sourceMappingURL=(.*)/, (match, value) => { 34 | return match.replace(value, 'index.mjs.map'); 35 | }); 36 | 37 | fs.writeFileSync(DESTINATION_ENTRY, contents, { encoding: 'utf8' }); 38 | 39 | console.log( 40 | `Copied ${getFileName(SOURCE_ENTRY)} to ${getFileName( 41 | DESTINATION_ENTRY 42 | )}` 43 | ); 44 | 45 | fs.copyFileSync(SOURCE_MAP, DESTINATION_MAP); 46 | 47 | console.log(`Copied ${SOURCE_MAP} to ${getFileName(DESTINATION_MAP)}`); 48 | 49 | fs.copyFileSync(SOURCE_TYPES, DESTINATION_TYPES); 50 | 51 | console.log( 52 | `Copied ${getFileName(SOURCE_TYPES)} to ${getFileName( 53 | DESTINATION_TYPES 54 | )}` 55 | ); 56 | } catch (error) { 57 | console.error(error); 58 | 59 | process.exit(1); 60 | } 61 | -------------------------------------------------------------------------------- /img/multiple-parameters.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/planttheidea/moize/b04c9e4b8a4c935694b3cc4803d2720adc0b79d0/img/multiple-parameters.png -------------------------------------------------------------------------------- /img/overall-average.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/planttheidea/moize/b04c9e4b8a4c935694b3cc4803d2720adc0b79d0/img/overall-average.png -------------------------------------------------------------------------------- /img/single-parameter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/planttheidea/moize/b04c9e4b8a4c935694b3cc4803d2720adc0b79d0/img/single-parameter.png -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 3 | import { 4 | Cache as BaseCache, 5 | Memoized as BaseMemoized, 6 | Options as BaseOptions, 7 | } from 'micro-memoize'; 8 | 9 | export type AnyFn = (...args: any[]) => any; 10 | export type Moizeable = AnyFn & Record; 11 | 12 | interface MoizedReactElement { 13 | type: any; 14 | props: any; 15 | key: string | number | null; 16 | } 17 | 18 | /** 19 | * @deprecated 20 | * 21 | * Use `AnyFn` instead, as it is more flexible and works better with type inference. 22 | */ 23 | export type Fn = ( 24 | ...args: Arg[] 25 | ) => Result; 26 | 27 | /** 28 | * @deprecated 29 | * 30 | * This should not longer need to be explicitly used, as inference of the function 31 | * returning the element should suffice. 32 | */ 33 | export type FunctionalComponent = (( 34 | props: Props 35 | ) => MoizedReactElement) & { 36 | displayName?: string; 37 | }; 38 | 39 | export type Key = Arg[]; 40 | export type Value = any; 41 | 42 | export type Cache = 43 | BaseCache; 44 | export type MicroMemoizeOptions = 45 | BaseOptions; 46 | 47 | export type Expiration = { 48 | expirationMethod: () => void; 49 | key: Key; 50 | timeoutId: ReturnType; 51 | }; 52 | 53 | export type OnCacheOperation = ( 54 | cache: Cache, 55 | options: Options, 56 | moized: (...args: any[]) => any 57 | ) => void; 58 | 59 | export type IsEqual = (cacheKeyArg: any, keyArg: any) => boolean; 60 | export type IsMatchingKey = (cacheKey: Key, key: Key) => boolean; 61 | export type OnExpire = (key: Key) => any; 62 | export type Serialize = (key: Key) => string[]; 63 | export type TransformKey = (key: Key) => Key; 64 | export type UpdateCacheForKey = (key: Key) => boolean; 65 | 66 | export type Options = Partial<{ 67 | isDeepEqual: boolean; 68 | isPromise: boolean; 69 | isReact: boolean; 70 | isSerialized: boolean; 71 | isShallowEqual: boolean; 72 | matchesArg: IsEqual; 73 | matchesKey: IsMatchingKey; 74 | maxAge: number; 75 | maxArgs: number; 76 | maxSize: number; 77 | onCacheAdd: OnCacheOperation; 78 | onCacheChange: OnCacheOperation; 79 | onCacheHit: OnCacheOperation; 80 | onExpire: OnExpire; 81 | profileName: string; 82 | serializer: Serialize; 83 | transformArgs: TransformKey; 84 | updateCacheForKey: UpdateCacheForKey; 85 | updateExpire: boolean; 86 | }>; 87 | 88 | export type StatsProfile = { 89 | calls: number; 90 | hits: number; 91 | }; 92 | 93 | export type StatsObject = { 94 | calls: number; 95 | hits: number; 96 | usage: string; 97 | }; 98 | 99 | export type GlobalStatsObject = StatsObject & { 100 | profiles?: Record; 101 | }; 102 | 103 | export type StatsCache = { 104 | anonymousProfileNameCounter: number; 105 | isCollectingStats: boolean; 106 | profiles: Record; 107 | }; 108 | 109 | export type Memoized = 110 | BaseMemoized; 111 | 112 | export type Moized< 113 | MoizeableFn extends Moizeable = Moizeable, 114 | CombinedOptions extends Options = Options 115 | > = Memoized & { 116 | // values 117 | _microMemoizeOptions: Pick< 118 | CombinedOptions, 119 | 'isPromise' | 'maxSize' | 'onCacheAdd' | 'onCacheChange' | 'onCacheHit' 120 | > & { 121 | isEqual: CombinedOptions['matchesArg']; 122 | isMatchingKey: CombinedOptions['matchesKey']; 123 | transformKey: CombinedOptions['transformArgs']; 124 | }; 125 | cache: Cache; 126 | cacheSnapshot: Cache; 127 | expirations: Expiration[]; 128 | expirationsSnapshot: Expiration[]; 129 | options: CombinedOptions; 130 | originalFunction: MoizeableFn; 131 | 132 | // react-specific values 133 | contextTypes?: Record; 134 | defaultProps?: Record; 135 | displayName?: string; 136 | propTypes: Record; 137 | 138 | // methods 139 | clear: () => void; 140 | clearStats: () => void; 141 | get: (key: Key) => any; 142 | getStats: () => StatsProfile; 143 | has: (key: Key) => boolean; 144 | isCollectingStats: () => boolean; 145 | isMoized: () => true; 146 | keys: () => Cache['keys']; 147 | remove: (key: Key) => void; 148 | set: (key: Key, value: any) => void; 149 | values: () => Cache['values']; 150 | }; 151 | 152 | export type MoizeConfiguration = { 153 | expirations: Expiration[]; 154 | options: Options; 155 | originalFunction: MoizeableFn; 156 | }; 157 | 158 | export type CurriedMoize = < 159 | CurriedFn extends Moizeable, 160 | CurriedOptions extends Options 161 | >( 162 | curriedFn: CurriedFn | CurriedOptions, 163 | curriedOptions?: CurriedOptions 164 | ) => 165 | | Moized 166 | | CurriedMoize; 167 | 168 | export interface MaxAge { 169 | (maxAge: MaxAge): Moizer<{ maxAge: MaxAge }>; 170 | ( 171 | maxAge: MaxAge, 172 | expireOptions: UpdateExpire 173 | ): Moizer<{ maxAge: MaxAge; updateExpire: UpdateExpire }>; 174 | ( 175 | maxAge: MaxAge, 176 | expireOptions: ExpireHandler 177 | ): Moizer<{ maxAge: MaxAge; onExpire: ExpireHandler }>; 178 | < 179 | MaxAge extends number, 180 | ExpireHandler extends OnExpire, 181 | ExpireOptions extends { 182 | onExpire: ExpireHandler; 183 | } 184 | >( 185 | maxAge: MaxAge, 186 | expireOptions: ExpireOptions 187 | ): Moizer<{ maxAge: MaxAge; onExpire: ExpireOptions['onExpire'] }>; 188 | < 189 | MaxAge extends number, 190 | UpdateExpire extends boolean, 191 | ExpireOptions extends { 192 | updateExpire: UpdateExpire; 193 | } 194 | >( 195 | maxAge: MaxAge, 196 | expireOptions: ExpireOptions 197 | ): Moizer<{ maxAge: MaxAge; updateExpire: UpdateExpire }>; 198 | < 199 | MaxAge extends number, 200 | ExpireHandler extends OnExpire, 201 | UpdateExpire extends boolean, 202 | ExpireOptions extends { 203 | onExpire: ExpireHandler; 204 | updateExpire: UpdateExpire; 205 | } 206 | >( 207 | maxAge: MaxAge, 208 | expireOptions: ExpireOptions 209 | ): Moizer<{ 210 | maxAge: MaxAge; 211 | onExpire: ExpireHandler; 212 | updateExpire: UpdateExpire; 213 | }>; 214 | } 215 | 216 | export interface Moizer< 217 | DefaultOptions extends Options = Options 218 | > { 219 | (fn: MoizeableFn): Moized< 220 | MoizeableFn, 221 | Options & DefaultOptions 222 | >; 223 | >( 224 | fn: MoizeableFn, 225 | options: PassedOptions 226 | ): Moized< 227 | MoizeableFn, 228 | Options & DefaultOptions & PassedOptions 229 | >; 230 | >(fn: MoizedFn): Moized< 231 | MoizedFn['fn'], 232 | Options & DefaultOptions 233 | >; 234 | < 235 | MoizedFn extends Moized, 236 | PassedOptions extends Options 237 | >( 238 | fn: MoizedFn, 239 | options: PassedOptions 240 | ): Moized< 241 | MoizedFn['fn'], 242 | Options & DefaultOptions & PassedOptions 243 | >; 244 | >( 245 | options: PassedOptions 246 | ): Moizer; 247 | } 248 | 249 | export interface Moize< 250 | DefaultOptions extends Options = Options 251 | > extends Moizer { 252 | clearStats: (profileName?: string) => void; 253 | collectStats: (isCollectingStats?: boolean) => void; 254 | compose: (...moizers: Array) => Moizer; 255 | deep: Moizer<{ isDeepEqual: true }>; 256 | getStats: (profileName?: string) => StatsObject; 257 | infinite: Moizer; 258 | isCollectingStats: () => boolean; 259 | isMoized: (value: any) => value is Moized; 260 | matchesArg: ( 261 | argMatcher: Matcher 262 | ) => Moizer<{ matchesArg: Matcher }>; 263 | matchesKey: ( 264 | keyMatcher: Matcher 265 | ) => Moizer<{ matchesKey: Matcher }>; 266 | maxAge: MaxAge; 267 | maxArgs: ( 268 | args: MaxArgs 269 | ) => Moizer<{ maxArgs: MaxArgs }>; 270 | maxSize: ( 271 | size: MaxSize 272 | ) => Moizer<{ maxSize: MaxSize }>; 273 | profile: ( 274 | profileName: ProfileName 275 | ) => Moizer<{ profileName: ProfileName }>; 276 | promise: Moizer<{ isPromise: true }>; 277 | react: Moizer<{ isReact: true }>; 278 | serialize: Moizer<{ isSerialized: true }>; 279 | serializeWith: ( 280 | serializer: Serializer 281 | ) => Moizer<{ isSerialized: true; serializer: Serializer }>; 282 | shallow: Moizer<{ isShallowEqual: true }>; 283 | transformArgs: ( 284 | transformer: Transformer 285 | ) => Moizer<{ transformArgs: Transformer }>; 286 | updateCacheForKey: ( 287 | updateCacheForKey: UpdateWhen 288 | ) => Moizer<{ updateCacheForKey: UpdateWhen }>; 289 | } 290 | 291 | declare const moize: Moize; 292 | 293 | export default moize; 294 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | coveragePathIgnorePatterns: ['node_modules', 'src/types.ts'], 3 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'], 4 | roots: [''], 5 | setupFiles: ['/jest.init.js'], 6 | testEnvironment: 'jsdom', 7 | testRegex: '/__tests__/.*\\.(ts|tsx|js)$', 8 | transform: { 9 | '\\.(js|ts|tsx)$': 'babel-jest', 10 | }, 11 | verbose: true, 12 | }; 13 | -------------------------------------------------------------------------------- /jest.init.js: -------------------------------------------------------------------------------- 1 | require('core-js'); 2 | require('regenerator-runtime/runtime'); 3 | -------------------------------------------------------------------------------- /mjs-test.mjs: -------------------------------------------------------------------------------- 1 | import moize from './mjs'; 2 | 3 | moize.collectStats(); 4 | 5 | const memoized = moize((a, b) => a + b, {profileName: 'memoized'}); 6 | 7 | const result = new Array(10).fill(null).map(() => memoized(1, 2)); 8 | 9 | console.log(result); 10 | console.log(memoized.getStats()); 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "planttheidea", 3 | "browser": "dist/moize.js", 4 | "browserslist": [ 5 | "defaults", 6 | "Explorer >= 9", 7 | "Safari >= 6", 8 | "Opera >= 15", 9 | "iOS >= 8", 10 | "Android >= 4" 11 | ], 12 | "bugs": { 13 | "url": "https://github.com/planttheidea/moize/issues" 14 | }, 15 | "dependencies": { 16 | "fast-equals": "^3.0.1", 17 | "micro-memoize": "^4.1.2" 18 | }, 19 | "description": "Blazing fast memoization based on all parameters passed", 20 | "devDependencies": { 21 | "@babel/cli": "^7.21.5", 22 | "@babel/core": "^7.21.8", 23 | "@babel/plugin-proposal-class-properties": "^7.18.6", 24 | "@babel/preset-env": "^7.21.5", 25 | "@babel/preset-react": "^7.18.6", 26 | "@babel/preset-typescript": "^7.21.5", 27 | "@rollup/plugin-babel": "^6.0.3", 28 | "@rollup/plugin-commonjs": "^24.1.0", 29 | "@rollup/plugin-node-resolve": "^15.0.2", 30 | "@rollup/plugin-terser": "^0.4.1", 31 | "@types/bluebird": "^3.5.38", 32 | "@types/eslint": "^8.37.0", 33 | "@types/jest": "^29.5.1", 34 | "@types/lodash": "^4.14.194", 35 | "@types/memoizee": "^0.4.8", 36 | "@types/react": "^18.2.6", 37 | "@types/react-dom": "^18.2.4", 38 | "@typescript-eslint/eslint-plugin": "^5.59.2", 39 | "@typescript-eslint/parser": "^5.59.2", 40 | "babel-jest": "^29.5.0", 41 | "babel-loader": "^9.1.2", 42 | "benchmark": "^2.1.4", 43 | "bluebird": "^3.7.2", 44 | "cli-table2": "^0.2.0", 45 | "core-js": "^3.30.2", 46 | "eslint": "^8.40.0", 47 | "eslint-friendly-formatter": "^4.0.1", 48 | "eslint-plugin-react": "^7.32.2", 49 | "eslint-webpack-plugin": "^4.0.1", 50 | "fast-memoize": "^2.5.2", 51 | "html-webpack-plugin": "^5.5.1", 52 | "in-publish": "^2.0.1", 53 | "ink-docstrap": "^1.3.2", 54 | "jest": "^29.5.0", 55 | "jest-environment-jsdom": "^29.5.0", 56 | "jsdoc": "^4.0.2", 57 | "jsdoc-babel": "^0.5.0", 58 | "lodash": "^4.17.21", 59 | "lru-memoize": "^1.1.0", 60 | "mem": "^8.1.1", 61 | "memoizee": "^0.4.15", 62 | "memoizerific": "^1.11.3", 63 | "ora": "^5.4.1", 64 | "prop-types": "^15.8.1", 65 | "q": "^1.5.1", 66 | "ramda": "^0.29.0", 67 | "react": "^18.2.0", 68 | "react-dom": "^18.2.0", 69 | "regenerator-runtime": "^0.13.11", 70 | "release-it": "^15.10.3", 71 | "rimraf": "^5.0.0", 72 | "rollup": "^3.21.5", 73 | "tslib": "^2.5.0", 74 | "typedoc": "^0.24.7", 75 | "typescript": "^5.0.4", 76 | "underscore": "^1.13.6", 77 | "webpack": "^5.82.0", 78 | "webpack-cli": "^5.1.0", 79 | "webpack-dev-server": "^4.15.0" 80 | }, 81 | "homepage": "https://github.com/planttheidea/moize#readme", 82 | "keywords": [ 83 | "cache", 84 | "expire", 85 | "lru", 86 | "memoize", 87 | "memoization", 88 | "optimize", 89 | "performance", 90 | "promise", 91 | "ttl" 92 | ], 93 | "license": "MIT", 94 | "main": "dist/moize.cjs.js", 95 | "module": "dist/moize.esm.js", 96 | "name": "moize", 97 | "repository": { 98 | "type": "git", 99 | "url": "git+https://github.com/planttheidea/moize.git" 100 | }, 101 | "scripts": { 102 | "benchmark": "npm run dist && node benchmark/index.js", 103 | "benchmark:alternative": "npm run transpile:lib -- --no-comments && BENCHMARK_SUITE=alternative node benchmark/index.js", 104 | "benchmark:array": "npm run transpile:lib -- --no-comments && BENCHMARK_SUITE=array node benchmark/index.js", 105 | "benchmark:object": "npm run transpile:lib -- --no-comments && BENCHMARK_SUITE=object node benchmark/index.js", 106 | "benchmark:primitive": "npm run transpile:lib -- --no-comments && BENCHMARK_SUITE=primitive node benchmark/index.js", 107 | "benchmark:react": "npm run transpile:lib -- --no-comments && BENCHMARK_SUITE=react node benchmark/index.js", 108 | "build": "NODE_ENV=production rollup -c --bundleConfigAsCjs", 109 | "clean:dist": "rimraf dist", 110 | "clean:docs": "rimraf docs", 111 | "clean:mjs": "rimraf mjs", 112 | "copy:mjs": "npm run clean:mjs && node ./es-to-mjs.js", 113 | "copy:types": "cp src/types.ts index.d.ts", 114 | "dev": "NODE_ENV=development webpack serve --progress --config=webpack/webpack.config.js", 115 | "dist": "npm run clean:dist && npm run build", 116 | "docs": "npm run clean:docs && typedoc", 117 | "lint": "NODE_ENV=test eslint src/*.ts", 118 | "lint:fix": "npm run lint -- --fix", 119 | "release": "release-it", 120 | "release:beta": "release-it --config=.release-it.beta.json", 121 | "release:scripts": "npm run lint && npm run typecheck && npm run test:coverage && npm run dist && npm run copy:mjs", 122 | "start": "npm run dev", 123 | "test": "NODE_ENV=test NODE_PATH=. jest", 124 | "test:coverage": "npm test -- --coverage", 125 | "test:watch": "npm test -- --watch", 126 | "typecheck": "tsc --noEmit" 127 | }, 128 | "sideEffects": false, 129 | "types": "./index.d.ts", 130 | "version": "6.1.6" 131 | } 132 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import resolve from '@rollup/plugin-node-resolve'; 2 | import babel from '@rollup/plugin-babel'; 3 | import terser from '@rollup/plugin-terser'; 4 | import pkg from './package.json'; 5 | 6 | const EXTERNALS = [ 7 | ...Object.keys(pkg.dependencies || {}), 8 | ...Object.keys(pkg.peerDependencies || {}), 9 | ]; 10 | 11 | const EXTENSIONS = ['.js', '.ts', '.tsx']; 12 | 13 | const DEFAULT_OUTPUT = { 14 | exports: 'default', 15 | globals: { 16 | 'fast-equals': 'fe', 17 | 'fast-stringify': 'stringify', 18 | 'micro-memoize': 'memoize', 19 | }, 20 | name: pkg.name, 21 | sourcemap: true, 22 | }; 23 | 24 | const DEFAULT_CONFIG = { 25 | external: EXTERNALS, 26 | input: 'src/index.ts', 27 | output: [ 28 | { ...DEFAULT_OUTPUT, file: pkg.browser, format: 'umd' }, 29 | { ...DEFAULT_OUTPUT, file: pkg.main, format: 'cjs' }, 30 | { ...DEFAULT_OUTPUT, file: pkg.module, format: 'es' }, 31 | ], 32 | plugins: [ 33 | resolve({ 34 | extensions: EXTENSIONS, 35 | mainFields: ['module', 'jsnext:main', 'main'], 36 | }), 37 | babel({ 38 | babelHelpers: 'bundled', 39 | exclude: 'node_modules/**', 40 | extensions: EXTENSIONS, 41 | include: ['src/*'], 42 | }), 43 | ], 44 | }; 45 | 46 | export default [ 47 | DEFAULT_CONFIG, 48 | { 49 | ...DEFAULT_CONFIG, 50 | output: { 51 | ...DEFAULT_OUTPUT, 52 | file: pkg.browser.replace('.js', '.min.js'), 53 | format: 'umd', 54 | }, 55 | plugins: [...DEFAULT_CONFIG.plugins, terser()], 56 | }, 57 | ]; 58 | -------------------------------------------------------------------------------- /src/component.ts: -------------------------------------------------------------------------------- 1 | import { copyStaticProperties } from './instance'; 2 | import { setName } from './utils'; 3 | 4 | import type { 5 | Moize, 6 | Moized as MoizedFunction, 7 | Moizeable, 8 | Options, 9 | } from '../index.d'; 10 | 11 | // This was stolen from React internals, which allows us to create React elements without needing 12 | // a dependency on the React library itself. 13 | const REACT_ELEMENT_TYPE = 14 | typeof Symbol === 'function' && Symbol.for 15 | ? Symbol.for('react.element') 16 | : 0xeac7; 17 | 18 | /** 19 | * @private 20 | * 21 | * @description 22 | * Create a component that memoizes based on `props` and legacy `context` 23 | * on a per-instance basis. This requires creating a component class to 24 | * store the memoized function. The cost is quite low, and avoids the 25 | * need to have access to the React dependency by basically re-creating 26 | * the basic essentials for a component class and the results of the 27 | * `createElement` function. 28 | * 29 | * @param moizer the top-level moize method 30 | * @param fn the component to memoize 31 | * @param options the memoization options 32 | * @returns the memoized component 33 | */ 34 | export function createMoizedComponent( 35 | moizer: Moize, 36 | fn: MoizeableFn, 37 | options: Options 38 | ) { 39 | /** 40 | * This is a hack override setting the necessary options 41 | * for a React component to be memoized. In the main `moize` 42 | * method, if the `isReact` option is set it is short-circuited 43 | * to call this function, and these overrides allow the 44 | * necessary transformKey method to be derived. 45 | * 46 | * The order is based on: 47 | * 1) Set the necessary aspects of transformKey for React components. 48 | * 2) Allow setting of other options and overrides of those aspects 49 | * if desired (for example, `isDeepEqual` will use deep equality). 50 | * 3) Always set `isReact` to false to prevent infinite loop. 51 | */ 52 | const reactMoizer = moizer({ 53 | maxArgs: 2, 54 | isShallowEqual: true, 55 | ...options, 56 | isReact: false, 57 | }); 58 | 59 | if (!fn.displayName) { 60 | // @ts-ignore - allow setting of displayName 61 | fn.displayName = fn.name || 'Component'; 62 | } 63 | 64 | function Moized, Context, Updater>( 65 | this: any, 66 | props: Props, 67 | context: Context, 68 | updater: Updater 69 | ) { 70 | this.props = props; 71 | this.context = context; 72 | this.updater = updater; 73 | 74 | this.MoizedComponent = reactMoizer(fn); 75 | } 76 | 77 | Moized.prototype.isReactComponent = {}; 78 | 79 | Moized.prototype.render = function (): ReturnType { 80 | return { 81 | $$typeof: REACT_ELEMENT_TYPE, 82 | type: this.MoizedComponent, 83 | props: this.props, 84 | ref: null, 85 | key: null, 86 | _owner: null, 87 | } as ReturnType; 88 | }; 89 | 90 | copyStaticProperties(fn, Moized, ['contextType', 'contextTypes']); 91 | 92 | Moized.displayName = `Moized(${fn.displayName || fn.name || 'Component'})`; 93 | 94 | setName(Moized as MoizedFunction, fn.name, options.profileName); 95 | 96 | return Moized; 97 | } 98 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | import type { AnyFn, Options } from '../index.d'; 2 | 3 | /** 4 | * @private 5 | * 6 | * @constant DEFAULT_OPTIONS 7 | */ 8 | export const DEFAULT_OPTIONS: Options = { 9 | isDeepEqual: false, 10 | isPromise: false, 11 | isReact: false, 12 | isSerialized: false, 13 | isShallowEqual: false, 14 | matchesArg: undefined, 15 | matchesKey: undefined, 16 | maxAge: undefined, 17 | maxArgs: undefined, 18 | maxSize: 1, 19 | onExpire: undefined, 20 | profileName: undefined, 21 | serializer: undefined, 22 | updateCacheForKey: undefined, 23 | transformArgs: undefined, 24 | updateExpire: false, 25 | }; 26 | -------------------------------------------------------------------------------- /src/instance.ts: -------------------------------------------------------------------------------- 1 | import { clearExpiration } from './maxAge'; 2 | import { clearStats, getStats } from './stats'; 3 | import { createFindKeyIndex } from './utils'; 4 | 5 | import type { 6 | Key, 7 | Memoized, 8 | Moizeable, 9 | MoizeConfiguration, 10 | Moized, 11 | Options, 12 | StatsProfile, 13 | } from '../index.d'; 14 | 15 | const ALWAYS_SKIPPED_PROPERTIES: Record = { 16 | arguments: true, 17 | callee: true, 18 | caller: true, 19 | constructor: true, 20 | length: true, 21 | name: true, 22 | prototype: true, 23 | }; 24 | 25 | /** 26 | * @private 27 | * 28 | * @description 29 | * copy the static properties from the original function to the moized 30 | * function 31 | * 32 | * @param originalFn the function copying from 33 | * @param newFn the function copying to 34 | * @param skippedProperties the list of skipped properties, if any 35 | */ 36 | export function copyStaticProperties< 37 | OriginalMoizeableFn extends Moizeable, 38 | NewMoizeableFn extends Moizeable 39 | >( 40 | originalFn: OriginalMoizeableFn, 41 | newFn: NewMoizeableFn, 42 | skippedProperties: string[] = [] 43 | ) { 44 | Object.getOwnPropertyNames(originalFn).forEach((property) => { 45 | if ( 46 | !ALWAYS_SKIPPED_PROPERTIES[property] && 47 | skippedProperties.indexOf(property) === -1 48 | ) { 49 | const descriptor = Object.getOwnPropertyDescriptor( 50 | originalFn, 51 | property 52 | ); 53 | 54 | if (descriptor.get || descriptor.set) { 55 | Object.defineProperty(newFn, property, descriptor); 56 | } else { 57 | // @ts-expect-error - properites may not align 58 | newFn[property] = originalFn[property]; 59 | } 60 | } 61 | }); 62 | } 63 | 64 | /** 65 | * @private 66 | * 67 | * @description 68 | * add methods to the moized fuction object that allow extra features 69 | * 70 | * @param memoized the memoized function from micro-memoize 71 | */ 72 | export function addInstanceMethods( 73 | memoized: Moizeable, 74 | { expirations }: MoizeConfiguration 75 | ) { 76 | const { options } = memoized; 77 | 78 | const findKeyIndex = createFindKeyIndex( 79 | options.isEqual, 80 | options.isMatchingKey 81 | ); 82 | 83 | const moized = memoized as unknown as Moized< 84 | MoizeableFn, 85 | Options 86 | >; 87 | 88 | moized.clear = function () { 89 | const { 90 | _microMemoizeOptions: { onCacheChange }, 91 | cache, 92 | } = moized; 93 | 94 | cache.keys.length = 0; 95 | cache.values.length = 0; 96 | 97 | if (onCacheChange) { 98 | onCacheChange(cache, moized.options, moized); 99 | } 100 | 101 | return true; 102 | }; 103 | 104 | moized.clearStats = function () { 105 | clearStats(moized.options.profileName); 106 | }; 107 | 108 | moized.get = function (key: Key) { 109 | const { 110 | _microMemoizeOptions: { transformKey }, 111 | cache, 112 | } = moized; 113 | 114 | const cacheKey = transformKey ? transformKey(key) : key; 115 | const keyIndex = findKeyIndex(cache.keys, cacheKey); 116 | 117 | return keyIndex !== -1 ? moized.apply(this, key) : undefined; 118 | }; 119 | 120 | moized.getStats = function (): StatsProfile { 121 | return getStats(moized.options.profileName); 122 | }; 123 | 124 | moized.has = function (key: Key) { 125 | const { transformKey } = moized._microMemoizeOptions; 126 | 127 | const cacheKey = transformKey ? transformKey(key) : key; 128 | 129 | return findKeyIndex(moized.cache.keys, cacheKey) !== -1; 130 | }; 131 | 132 | moized.keys = function () { 133 | return moized.cacheSnapshot.keys; 134 | }; 135 | 136 | moized.remove = function (key: Key) { 137 | const { 138 | _microMemoizeOptions: { onCacheChange, transformKey }, 139 | cache, 140 | } = moized; 141 | 142 | const keyIndex = findKeyIndex( 143 | cache.keys, 144 | transformKey ? transformKey(key) : key 145 | ); 146 | 147 | if (keyIndex === -1) { 148 | return false; 149 | } 150 | 151 | const existingKey = cache.keys[keyIndex]; 152 | 153 | cache.keys.splice(keyIndex, 1); 154 | cache.values.splice(keyIndex, 1); 155 | 156 | if (onCacheChange) { 157 | onCacheChange(cache, moized.options, moized); 158 | } 159 | 160 | clearExpiration(expirations, existingKey, true); 161 | 162 | return true; 163 | }; 164 | 165 | moized.set = function (key: Key, value: any) { 166 | const { _microMemoizeOptions, cache, options } = moized; 167 | const { onCacheAdd, onCacheChange, transformKey } = 168 | _microMemoizeOptions; 169 | 170 | const cacheKey = transformKey ? transformKey(key) : key; 171 | const keyIndex = findKeyIndex(cache.keys, cacheKey); 172 | 173 | if (keyIndex === -1) { 174 | const cutoff = options.maxSize - 1; 175 | 176 | if (cache.size > cutoff) { 177 | cache.keys.length = cutoff; 178 | cache.values.length = cutoff; 179 | } 180 | 181 | cache.keys.unshift(cacheKey); 182 | cache.values.unshift(value); 183 | 184 | if (options.isPromise) { 185 | cache.updateAsyncCache(moized); 186 | } 187 | 188 | if (onCacheAdd) { 189 | onCacheAdd(cache, options, moized); 190 | } 191 | 192 | if (onCacheChange) { 193 | onCacheChange(cache, options, moized); 194 | } 195 | } else { 196 | const existingKey = cache.keys[keyIndex]; 197 | 198 | cache.values[keyIndex] = value; 199 | 200 | if (keyIndex > 0) { 201 | cache.orderByLru(existingKey, value, keyIndex); 202 | } 203 | 204 | if (options.isPromise) { 205 | cache.updateAsyncCache(moized); 206 | } 207 | 208 | if (typeof onCacheChange === 'function') { 209 | onCacheChange(cache, options, moized); 210 | } 211 | } 212 | }; 213 | 214 | moized.values = function () { 215 | return moized.cacheSnapshot.values; 216 | }; 217 | } 218 | 219 | /** 220 | * @private 221 | * 222 | * @description 223 | * add propeties to the moized fuction object that surfaces extra information 224 | * 225 | * @param memoized the memoized function 226 | * @param expirations the list of expirations for cache items 227 | * @param options the options passed to the moizer 228 | * @param originalFunction the function that is being memoized 229 | */ 230 | export function addInstanceProperties( 231 | memoized: Memoized, 232 | { 233 | expirations, 234 | options: moizeOptions, 235 | originalFunction, 236 | }: MoizeConfiguration 237 | ) { 238 | const { options: microMemoizeOptions } = memoized; 239 | 240 | Object.defineProperties(memoized, { 241 | _microMemoizeOptions: { 242 | configurable: true, 243 | get() { 244 | return microMemoizeOptions; 245 | }, 246 | }, 247 | 248 | cacheSnapshot: { 249 | configurable: true, 250 | get() { 251 | const { cache: currentCache } = memoized; 252 | 253 | return { 254 | keys: currentCache.keys.slice(0), 255 | size: currentCache.size, 256 | values: currentCache.values.slice(0), 257 | }; 258 | }, 259 | }, 260 | 261 | expirations: { 262 | configurable: true, 263 | get() { 264 | return expirations; 265 | }, 266 | }, 267 | 268 | expirationsSnapshot: { 269 | configurable: true, 270 | get() { 271 | return expirations.slice(0); 272 | }, 273 | }, 274 | 275 | isMoized: { 276 | configurable: true, 277 | get() { 278 | return true; 279 | }, 280 | }, 281 | 282 | options: { 283 | configurable: true, 284 | get() { 285 | return moizeOptions; 286 | }, 287 | }, 288 | 289 | originalFunction: { 290 | configurable: true, 291 | get() { 292 | return originalFunction; 293 | }, 294 | }, 295 | }); 296 | 297 | const moized = memoized as unknown as Moized< 298 | MoizeableFn, 299 | Options 300 | >; 301 | 302 | copyStaticProperties(originalFunction, moized); 303 | } 304 | 305 | /** 306 | * @private 307 | * 308 | * @description 309 | * add methods and properties to the memoized function for more features 310 | * 311 | * @param memoized the memoized function 312 | * @param configuration the configuration object for the instance 313 | * @returns the memoized function passed 314 | */ 315 | export function createMoizeInstance< 316 | MoizeableFn extends Moizeable, 317 | CombinedOptions extends Options 318 | >( 319 | memoized: Memoized, 320 | configuration: MoizeConfiguration 321 | ) { 322 | addInstanceMethods(memoized, configuration); 323 | addInstanceProperties(memoized, configuration); 324 | 325 | return memoized as Moized; 326 | } 327 | -------------------------------------------------------------------------------- /src/maxAge.ts: -------------------------------------------------------------------------------- 1 | import { createFindKeyIndex, findExpirationIndex } from './utils'; 2 | 3 | import type { 4 | AnyFn, 5 | Cache, 6 | Expiration, 7 | IsEqual, 8 | IsMatchingKey, 9 | Key, 10 | OnCacheOperation, 11 | Options, 12 | } from '../index.d'; 13 | 14 | /** 15 | * @private 16 | * 17 | * @description 18 | * clear an active expiration and remove it from the list if applicable 19 | * 20 | * @param expirations the list of expirations 21 | * @param key the key to clear 22 | * @param shouldRemove should the expiration be removed from the list 23 | */ 24 | export function clearExpiration( 25 | expirations: Expiration[], 26 | key: Key, 27 | shouldRemove?: boolean 28 | ) { 29 | const expirationIndex = findExpirationIndex(expirations, key); 30 | 31 | if (expirationIndex !== -1) { 32 | clearTimeout(expirations[expirationIndex].timeoutId); 33 | 34 | if (shouldRemove) { 35 | expirations.splice(expirationIndex, 1); 36 | } 37 | } 38 | } 39 | 40 | /** 41 | * @private 42 | * 43 | * @description 44 | * Create the timeout for the given expiration method. If the ability to `unref` 45 | * exists, then apply it to avoid process locks in NodeJS. 46 | * 47 | * @param expirationMethod the method to fire upon expiration 48 | * @param maxAge the time to expire after 49 | * @returns the timeout ID 50 | */ 51 | export function createTimeout(expirationMethod: () => void, maxAge: number) { 52 | const timeoutId = setTimeout(expirationMethod, maxAge); 53 | 54 | if (typeof timeoutId.unref === 'function') { 55 | timeoutId.unref(); 56 | } 57 | 58 | return timeoutId; 59 | } 60 | 61 | /** 62 | * @private 63 | * 64 | * @description 65 | * create a function that, when an item is added to the cache, adds an expiration for it 66 | * 67 | * @param expirations the mutable expirations array 68 | * @param options the options passed on initialization 69 | * @param isEqual the function to check argument equality 70 | * @param isMatchingKey the function to check complete key equality 71 | * @returns the onCacheAdd function to handle expirations 72 | */ 73 | export function createOnCacheAddSetExpiration( 74 | expirations: Expiration[], 75 | options: Options, 76 | isEqual: IsEqual, 77 | isMatchingKey: IsMatchingKey 78 | ): OnCacheOperation { 79 | const { maxAge } = options; 80 | 81 | return function onCacheAdd( 82 | cache: Cache, 83 | moizedOptions: Options, 84 | moized: MoizeableFn 85 | ) { 86 | const key: any = cache.keys[0]; 87 | 88 | if (findExpirationIndex(expirations, key) === -1) { 89 | const expirationMethod = function () { 90 | const findKeyIndex = createFindKeyIndex(isEqual, isMatchingKey); 91 | 92 | const keyIndex: number = findKeyIndex(cache.keys, key); 93 | const value: any = cache.values[keyIndex]; 94 | 95 | if (~keyIndex) { 96 | cache.keys.splice(keyIndex, 1); 97 | cache.values.splice(keyIndex, 1); 98 | 99 | if (typeof options.onCacheChange === 'function') { 100 | options.onCacheChange(cache, moizedOptions, moized); 101 | } 102 | } 103 | 104 | clearExpiration(expirations, key, true); 105 | 106 | if ( 107 | typeof options.onExpire === 'function' && 108 | options.onExpire(key) === false 109 | ) { 110 | cache.keys.unshift(key); 111 | cache.values.unshift(value); 112 | 113 | onCacheAdd(cache, moizedOptions, moized); 114 | 115 | if (typeof options.onCacheChange === 'function') { 116 | options.onCacheChange(cache, moizedOptions, moized); 117 | } 118 | } 119 | }; 120 | 121 | expirations.push({ 122 | expirationMethod, 123 | key, 124 | timeoutId: createTimeout(expirationMethod, maxAge), 125 | }); 126 | } 127 | }; 128 | } 129 | 130 | /** 131 | * @private 132 | * 133 | * @description 134 | * creates a function that, when a cache item is hit, reset the expiration 135 | * 136 | * @param expirations the mutable expirations array 137 | * @param options the options passed on initialization 138 | * @returns the onCacheAdd function to handle expirations 139 | */ 140 | export function createOnCacheHitResetExpiration( 141 | expirations: Expiration[], 142 | options: Options 143 | ): OnCacheOperation { 144 | return function onCacheHit(cache: Cache) { 145 | const key = cache.keys[0]; 146 | const expirationIndex = findExpirationIndex(expirations, key); 147 | 148 | if (~expirationIndex) { 149 | clearExpiration(expirations, key, false); 150 | 151 | expirations[expirationIndex].timeoutId = createTimeout( 152 | expirations[expirationIndex].expirationMethod, 153 | options.maxAge 154 | ); 155 | } 156 | }; 157 | } 158 | 159 | /** 160 | * @private 161 | * 162 | * @description 163 | * get the micro-memoize options specific to the maxAge option 164 | * 165 | * @param expirations the expirations for the memoized function 166 | * @param options the options passed to the moizer 167 | * @param isEqual the function to test equality of the key on a per-argument basis 168 | * @param isMatchingKey the function to test equality of the whole key 169 | * @returns the object of options based on the entries passed 170 | */ 171 | export function getMaxAgeOptions( 172 | expirations: Expiration[], 173 | options: Options, 174 | isEqual: IsEqual, 175 | isMatchingKey: IsMatchingKey 176 | ): { 177 | onCacheAdd: OnCacheOperation | undefined; 178 | onCacheHit: OnCacheOperation | undefined; 179 | } { 180 | const onCacheAdd = 181 | typeof options.maxAge === 'number' && isFinite(options.maxAge) 182 | ? createOnCacheAddSetExpiration( 183 | expirations, 184 | options, 185 | isEqual, 186 | isMatchingKey 187 | ) 188 | : undefined; 189 | 190 | return { 191 | onCacheAdd, 192 | onCacheHit: 193 | onCacheAdd && options.updateExpire 194 | ? createOnCacheHitResetExpiration(expirations, options) 195 | : undefined, 196 | }; 197 | } 198 | -------------------------------------------------------------------------------- /src/maxArgs.ts: -------------------------------------------------------------------------------- 1 | import type { Key } from '../index.d'; 2 | 3 | export function createGetInitialArgs(size: number) { 4 | /** 5 | * @private 6 | * 7 | * @description 8 | * take the first N number of items from the array (faster than slice) 9 | * 10 | * @param args the args to take from 11 | * @returns the shortened list of args as an array 12 | */ 13 | return function (args: Key): Key { 14 | if (size >= args.length) { 15 | return args; 16 | } 17 | 18 | if (size === 0) { 19 | return []; 20 | } 21 | 22 | if (size === 1) { 23 | return [args[0]]; 24 | } 25 | 26 | if (size === 2) { 27 | return [args[0], args[1]]; 28 | } 29 | 30 | if (size === 3) { 31 | return [args[0], args[1], args[2]]; 32 | } 33 | 34 | const clone = []; 35 | 36 | for (let index = 0; index < size; index++) { 37 | clone[index] = args[index]; 38 | } 39 | 40 | return clone; 41 | }; 42 | } 43 | -------------------------------------------------------------------------------- /src/options.ts: -------------------------------------------------------------------------------- 1 | import { deepEqual, sameValueZeroEqual, shallowEqual } from 'fast-equals'; 2 | import { createGetInitialArgs } from './maxArgs'; 3 | import { getIsSerializedKeyEqual, getSerializerFunction } from './serialize'; 4 | import { compose } from './utils'; 5 | 6 | import type { 7 | Cache, 8 | IsEqual, 9 | IsMatchingKey, 10 | MicroMemoizeOptions, 11 | Moizeable, 12 | Moized, 13 | OnCacheOperation, 14 | Options, 15 | TransformKey, 16 | } from '../index.d'; 17 | 18 | export function createOnCacheOperation( 19 | fn?: OnCacheOperation 20 | ): OnCacheOperation { 21 | if (typeof fn === 'function') { 22 | return ( 23 | _cacheIgnored: Cache, 24 | _microMemoizeOptionsIgnored: MicroMemoizeOptions, 25 | memoized: Moized 26 | ): void => fn(memoized.cache, memoized.options, memoized); 27 | } 28 | } 29 | 30 | /** 31 | * @private 32 | * 33 | * @description 34 | * get the isEqual method passed to micro-memoize 35 | * 36 | * @param options the options passed to the moizer 37 | * @returns the isEqual method to apply 38 | */ 39 | export function getIsEqual( 40 | options: Options 41 | ): IsEqual { 42 | return ( 43 | options.matchesArg || 44 | (options.isDeepEqual && deepEqual) || 45 | (options.isShallowEqual && shallowEqual) || 46 | sameValueZeroEqual 47 | ); 48 | } 49 | 50 | /** 51 | * @private 52 | * 53 | * @description 54 | * get the isEqual method passed to micro-memoize 55 | * 56 | * @param options the options passed to the moizer 57 | * @returns the isEqual method to apply 58 | */ 59 | export function getIsMatchingKey( 60 | options: Options 61 | ): IsMatchingKey | undefined { 62 | return ( 63 | options.matchesKey || 64 | (options.isSerialized && getIsSerializedKeyEqual) || 65 | undefined 66 | ); 67 | } 68 | 69 | /** 70 | * @private 71 | * 72 | * @description 73 | * get the function that will transform the key based on the arguments passed 74 | * 75 | * @param options the options passed to the moizer 76 | * @returns the function to transform the key with 77 | */ 78 | export function getTransformKey( 79 | options: Options 80 | ): TransformKey | undefined { 81 | return compose( 82 | options.isSerialized && getSerializerFunction(options), 83 | typeof options.transformArgs === 'function' && options.transformArgs, 84 | typeof options.maxArgs === 'number' && 85 | createGetInitialArgs(options.maxArgs) 86 | ) as TransformKey; 87 | } 88 | -------------------------------------------------------------------------------- /src/serialize.ts: -------------------------------------------------------------------------------- 1 | import type { Key, Moizeable, Options } from '../index.d'; 2 | 3 | /** 4 | * @function getCutoff 5 | * 6 | * @description 7 | * faster `Array.prototype.indexOf` implementation build for slicing / splicing 8 | * 9 | * @param array the array to match the value in 10 | * @param value the value to match 11 | * @returns the matching index, or -1 12 | */ 13 | function getCutoff(array: any[], value: any) { 14 | const { length } = array; 15 | 16 | for (let index = 0; index < length; ++index) { 17 | if (array[index] === value) { 18 | return index + 1; 19 | } 20 | } 21 | 22 | return 0; 23 | } 24 | 25 | /** 26 | * @private 27 | * 28 | * @description 29 | * custom replacer for the stringify function 30 | * 31 | * @returns if function then toString of it, else the value itself 32 | */ 33 | export function createDefaultReplacer() { 34 | const cache: any[] = []; 35 | const keys: string[] = []; 36 | 37 | return function defaultReplacer(key: string, value: any) { 38 | const type = typeof value; 39 | 40 | if (type === 'function' || type === 'symbol') { 41 | return value.toString(); 42 | } 43 | 44 | if (typeof value === 'object') { 45 | if (cache.length) { 46 | const thisCutoff = getCutoff(cache, this); 47 | 48 | if (thisCutoff === 0) { 49 | cache[cache.length] = this; 50 | } else { 51 | cache.splice(thisCutoff); 52 | keys.splice(thisCutoff); 53 | } 54 | 55 | keys[keys.length] = key; 56 | 57 | const valueCutoff = getCutoff(cache, value); 58 | 59 | if (valueCutoff !== 0) { 60 | return `[ref=${ 61 | keys.slice(0, valueCutoff).join('.') || '.' 62 | }]`; 63 | } 64 | } else { 65 | cache[0] = value; 66 | keys[0] = key; 67 | } 68 | 69 | return value; 70 | } 71 | 72 | return '' + value; 73 | }; 74 | } 75 | 76 | /** 77 | * @private 78 | * 79 | * @description 80 | * get the stringified version of the argument passed 81 | * 82 | * @param arg argument to stringify 83 | * @returns the stringified argument 84 | */ 85 | export function getStringifiedArgument(arg: Type) { 86 | const typeOfArg = typeof arg; 87 | 88 | return arg && (typeOfArg === 'object' || typeOfArg === 'function') 89 | ? JSON.stringify(arg, createDefaultReplacer()) 90 | : arg; 91 | } 92 | 93 | /** 94 | * @private 95 | * 96 | * @description 97 | * serialize the arguments passed 98 | * 99 | * @param options the options passed to the moizer 100 | * @param options.maxArgs the cap on the number of arguments used in serialization 101 | * @returns argument serialization method 102 | */ 103 | export function defaultArgumentSerializer(args: Key) { 104 | let key = '|'; 105 | 106 | for (let index = 0; index < args.length; index++) { 107 | key += getStringifiedArgument(args[index]) + '|'; 108 | } 109 | 110 | return [key]; 111 | } 112 | 113 | /** 114 | * @private 115 | * 116 | * @description 117 | * based on the options passed, either use the serializer passed or generate the internal one 118 | * 119 | * @param options the options passed to the moized function 120 | * @returns the function to use in serializing the arguments 121 | */ 122 | export function getSerializerFunction( 123 | options: Options 124 | ) { 125 | return typeof options.serializer === 'function' 126 | ? options.serializer 127 | : defaultArgumentSerializer; 128 | } 129 | 130 | /** 131 | * @private 132 | * 133 | * @description 134 | * are the serialized keys equal to one another 135 | * 136 | * @param cacheKey the cache key to compare 137 | * @param key the key to test 138 | * @returns are the keys equal 139 | */ 140 | export function getIsSerializedKeyEqual(cacheKey: Key, key: Key) { 141 | return cacheKey[0] === key[0]; 142 | } 143 | -------------------------------------------------------------------------------- /src/stats.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | GlobalStatsObject, 3 | Moizeable, 4 | OnCacheOperation, 5 | Options, 6 | StatsCache, 7 | StatsProfile, 8 | } from '../index.d'; 9 | 10 | export const statsCache: StatsCache = { 11 | anonymousProfileNameCounter: 1, 12 | isCollectingStats: false, 13 | profiles: {}, 14 | }; 15 | 16 | let hasWarningDisplayed = false; 17 | 18 | export function clearStats(profileName?: string) { 19 | if (profileName) { 20 | delete statsCache.profiles[profileName]; 21 | } else { 22 | statsCache.profiles = {}; 23 | } 24 | } 25 | 26 | /** 27 | * @private 28 | * 29 | * @description 30 | * activate stats collection 31 | * 32 | * @param isCollectingStats should stats be collected 33 | */ 34 | export function collectStats(isCollectingStats = true) { 35 | statsCache.isCollectingStats = isCollectingStats; 36 | } 37 | 38 | /** 39 | * @private 40 | * 41 | * @description 42 | * create a function that increments the number of calls for the specific profile 43 | */ 44 | export function createOnCacheAddIncrementCalls( 45 | options: Options 46 | ) { 47 | const { profileName } = options; 48 | 49 | return function () { 50 | if (profileName && !statsCache.profiles[profileName]) { 51 | statsCache.profiles[profileName] = { 52 | calls: 0, 53 | hits: 0, 54 | }; 55 | } 56 | 57 | statsCache.profiles[profileName].calls++; 58 | }; 59 | } 60 | 61 | /** 62 | * @private 63 | * 64 | * @description 65 | * create a function that increments the number of calls and cache hits for the specific profile 66 | */ 67 | export function createOnCacheHitIncrementCallsAndHits< 68 | MoizeableFn extends Moizeable 69 | >(options: Options) { 70 | return function () { 71 | const { profiles } = statsCache; 72 | const { profileName } = options; 73 | 74 | if (!profiles[profileName]) { 75 | profiles[profileName] = { 76 | calls: 0, 77 | hits: 0, 78 | }; 79 | } 80 | 81 | profiles[profileName].calls++; 82 | profiles[profileName].hits++; 83 | }; 84 | } 85 | 86 | /** 87 | * @private 88 | * 89 | * @description 90 | * get the profileName for the function when one is not provided 91 | * 92 | * @param fn the function to be memoized 93 | * @returns the derived profileName for the function 94 | */ 95 | export function getDefaultProfileName( 96 | fn: MoizeableFn 97 | ) { 98 | return ( 99 | fn.displayName || 100 | fn.name || 101 | `Anonymous ${statsCache.anonymousProfileNameCounter++}` 102 | ); 103 | } 104 | 105 | /** 106 | * @private 107 | * 108 | * @description 109 | * get the usage percentage based on the number of hits and total calls 110 | * 111 | * @param calls the number of calls made 112 | * @param hits the number of cache hits when called 113 | * @returns the usage as a percentage string 114 | */ 115 | export function getUsagePercentage(calls: number, hits: number) { 116 | return calls ? `${((hits / calls) * 100).toFixed(4)}%` : '0.0000%'; 117 | } 118 | 119 | /** 120 | * @private 121 | * 122 | * @description 123 | * get the statistics for a given method or all methods 124 | * 125 | * @param [profileName] the profileName to get the statistics for (get all when not provided) 126 | * @returns the object with stats information 127 | */ 128 | export function getStats(profileName?: string): GlobalStatsObject { 129 | if (!statsCache.isCollectingStats && !hasWarningDisplayed) { 130 | console.warn( 131 | 'Stats are not currently being collected, please run "collectStats" to enable them.' 132 | ); // eslint-disable-line no-console 133 | 134 | hasWarningDisplayed = true; 135 | } 136 | 137 | const { profiles } = statsCache; 138 | 139 | if (profileName) { 140 | if (!profiles[profileName]) { 141 | return { 142 | calls: 0, 143 | hits: 0, 144 | usage: '0.0000%', 145 | }; 146 | } 147 | 148 | const { [profileName]: profile } = profiles; 149 | 150 | return { 151 | ...profile, 152 | usage: getUsagePercentage(profile.calls, profile.hits), 153 | }; 154 | } 155 | 156 | const completeStats: StatsProfile = Object.keys(statsCache.profiles).reduce( 157 | (completeProfiles, profileName) => { 158 | completeProfiles.calls += profiles[profileName].calls; 159 | completeProfiles.hits += profiles[profileName].hits; 160 | 161 | return completeProfiles; 162 | }, 163 | { 164 | calls: 0, 165 | hits: 0, 166 | } 167 | ); 168 | 169 | return { 170 | ...completeStats, 171 | profiles: Object.keys(profiles).reduce( 172 | (computedProfiles, profileName) => { 173 | computedProfiles[profileName] = getStats(profileName); 174 | 175 | return computedProfiles; 176 | }, 177 | {} as Record 178 | ), 179 | usage: getUsagePercentage(completeStats.calls, completeStats.hits), 180 | }; 181 | } 182 | 183 | /** 184 | * @private 185 | * 186 | * @function getStatsOptions 187 | * 188 | * @description 189 | * get the options specific to storing statistics 190 | * 191 | * @param {Options} options the options passed to the moizer 192 | * @returns {Object} the options specific to keeping stats 193 | */ 194 | export function getStatsOptions( 195 | options: Options 196 | ): { 197 | onCacheAdd?: OnCacheOperation; 198 | onCacheHit?: OnCacheOperation; 199 | } { 200 | return statsCache.isCollectingStats 201 | ? { 202 | onCacheAdd: createOnCacheAddIncrementCalls(options), 203 | onCacheHit: createOnCacheHitIncrementCallsAndHits(options), 204 | } 205 | : {}; 206 | } 207 | -------------------------------------------------------------------------------- /src/updateCacheForKey.ts: -------------------------------------------------------------------------------- 1 | import { copyStaticProperties } from './instance'; 2 | 3 | import type { Moized } from '../index.d'; 4 | 5 | export function createRefreshableMoized( 6 | moized: MoizedFn 7 | ) { 8 | const { 9 | options: { updateCacheForKey }, 10 | } = moized; 11 | 12 | /** 13 | * @private 14 | * 15 | * @description 16 | * Wrapper around already-`moize`d function which will intercept the memoization 17 | * and call the underlying function directly with the purpose of updating the cache 18 | * for the given key. 19 | * 20 | * Promise values use a tweak of the logic that exists at cache.updateAsyncCache, which 21 | * reverts to the original value if the promise is rejected and there was already a cached 22 | * value. 23 | */ 24 | const refreshableMoized = function refreshableMoized( 25 | this: any, 26 | ...args: Parameters 27 | ) { 28 | if (!updateCacheForKey(args)) { 29 | return moized.apply(this, args); 30 | } 31 | 32 | const result = moized.fn.apply(this, args); 33 | 34 | moized.set(args, result); 35 | 36 | return result; 37 | } as typeof moized; 38 | 39 | copyStaticProperties(moized, refreshableMoized); 40 | 41 | return refreshableMoized; 42 | } 43 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import { DEFAULT_OPTIONS } from './constants'; 2 | 3 | import type { 4 | AnyFn, 5 | Expiration, 6 | IsEqual, 7 | IsMatchingKey, 8 | Key, 9 | Moizeable, 10 | Moized, 11 | Options, 12 | } from '../index.d'; 13 | 14 | /** 15 | * @private 16 | * 17 | * @description 18 | * method to combine functions and return a single function that fires them all 19 | * 20 | * @param functions the functions to compose 21 | * @returns the composed function 22 | */ 23 | export function combine( 24 | ...functions: Array<(...args: Args) => any> 25 | ): ((...args: Args) => Result) | undefined { 26 | return functions.reduce(function (f: any, g: any) { 27 | if (typeof f === 'function') { 28 | return typeof g === 'function' 29 | ? function (this: any) { 30 | f.apply(this, arguments); 31 | g.apply(this, arguments); 32 | } 33 | : f; 34 | } 35 | 36 | if (typeof g === 'function') { 37 | return g; 38 | } 39 | }); 40 | } 41 | 42 | /** 43 | * @private 44 | * 45 | * @description 46 | * method to compose functions and return a single function 47 | * 48 | * @param functions the functions to compose 49 | * @returns the composed function 50 | */ 51 | export function compose(...functions: Method[]): Method { 52 | return functions.reduce(function (f: any, g: any) { 53 | if (typeof f === 'function') { 54 | return typeof g === 'function' 55 | ? function (this: any) { 56 | return f(g.apply(this, arguments)); 57 | } 58 | : f; 59 | } 60 | 61 | if (typeof g === 'function') { 62 | return g; 63 | } 64 | }); 65 | } 66 | 67 | /** 68 | * @private 69 | * 70 | * @description 71 | * find the index of the expiration based on the key 72 | * 73 | * @param expirations the list of expirations 74 | * @param key the key to match 75 | * @returns the index of the expiration 76 | */ 77 | export function findExpirationIndex(expirations: Expiration[], key: Key) { 78 | for (let index = 0; index < expirations.length; index++) { 79 | if (expirations[index].key === key) { 80 | return index; 81 | } 82 | } 83 | 84 | return -1; 85 | } 86 | 87 | /** 88 | * @private 89 | * 90 | * @description 91 | * create function that finds the index of the key in the list of cache keys 92 | * 93 | * @param isEqual the function to test individual argument equality 94 | * @param isMatchingKey the function to test full key equality 95 | * @returns the function that finds the index of the key 96 | */ 97 | export function createFindKeyIndex( 98 | isEqual: IsEqual, 99 | isMatchingKey: IsMatchingKey | undefined 100 | ) { 101 | const areKeysEqual: IsMatchingKey = 102 | typeof isMatchingKey === 'function' 103 | ? isMatchingKey 104 | : function (cacheKey: Key, key: Key) { 105 | for (let index = 0; index < key.length; index++) { 106 | if (!isEqual(cacheKey[index], key[index])) { 107 | return false; 108 | } 109 | } 110 | 111 | return true; 112 | }; 113 | 114 | return function (keys: Key[], key: Key) { 115 | for (let keysIndex = 0; keysIndex < keys.length; keysIndex++) { 116 | if ( 117 | keys[keysIndex].length === key.length && 118 | areKeysEqual(keys[keysIndex], key) 119 | ) { 120 | return keysIndex; 121 | } 122 | } 123 | 124 | return -1; 125 | }; 126 | } 127 | 128 | type MergedOptions< 129 | OriginalOptions extends Options, 130 | NewOptions extends Options 131 | > = Omit & NewOptions; 132 | 133 | /** 134 | * @private 135 | * 136 | * @description 137 | * merge two options objects, combining or composing functions as necessary 138 | * 139 | * @param originalOptions the options that already exist on the method 140 | * @param newOptions the new options to merge 141 | * @returns the merged options 142 | */ 143 | export function mergeOptions< 144 | OriginalOptions extends Options, 145 | NewOptions extends Options 146 | >( 147 | originalOptions: OriginalOptions, 148 | newOptions: NewOptions | undefined 149 | ): MergedOptions { 150 | if (!newOptions || newOptions === DEFAULT_OPTIONS) { 151 | return originalOptions as unknown as MergedOptions< 152 | OriginalOptions, 153 | NewOptions 154 | >; 155 | } 156 | 157 | return { 158 | ...originalOptions, 159 | ...newOptions, 160 | onCacheAdd: combine(originalOptions.onCacheAdd, newOptions.onCacheAdd), 161 | onCacheChange: combine( 162 | originalOptions.onCacheChange, 163 | newOptions.onCacheChange 164 | ), 165 | onCacheHit: combine(originalOptions.onCacheHit, newOptions.onCacheHit), 166 | transformArgs: compose( 167 | originalOptions.transformArgs, 168 | newOptions.transformArgs 169 | ), 170 | }; 171 | } 172 | 173 | export function isMoized( 174 | fn: Moizeable | Moized | Options 175 | ): fn is Moized { 176 | return typeof fn === 'function' && (fn as Moizeable).isMoized; 177 | } 178 | 179 | export function setName( 180 | fn: Moized, 181 | originalFunctionName: string, 182 | profileName: string 183 | ) { 184 | try { 185 | const name = profileName || originalFunctionName || 'anonymous'; 186 | 187 | Object.defineProperty(fn, 'name', { 188 | configurable: true, 189 | enumerable: false, 190 | value: `moized(${name})`, 191 | writable: true, 192 | }); 193 | } catch { 194 | // For engines where `function.name` is not configurable, do nothing. 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, 4 | "baseUrl": "src", 5 | "esModuleInterop": true, 6 | "jsx": "react", 7 | "lib": ["dom", "es2015"], 8 | "module": "esNext", 9 | "moduleResolution": "node", 10 | "noImplicitAny": true, 11 | "outDir": "./dist", 12 | "sourceMap": true, 13 | "target": "es5" 14 | }, 15 | "exclude": ["node_modules"], 16 | "include": ["src/*", "__tests__/*"], 17 | "typedocOptions": { 18 | "out": "docs", 19 | "entryPoints": ["src/index.ts"] 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /webpack/webpack.config.js: -------------------------------------------------------------------------------- 1 | const ESLintWebpackPlugin = require('eslint-webpack-plugin'); 2 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 3 | const path = require('path'); 4 | const webpack = require('webpack'); 5 | 6 | const ROOT = path.resolve(__dirname, '..'); 7 | const PORT = 3000; 8 | 9 | module.exports = { 10 | cache: true, 11 | 12 | devServer: { 13 | host: 'localhost', 14 | port: PORT, 15 | }, 16 | 17 | devtool: 'source-map', 18 | 19 | entry: [path.resolve(ROOT, 'DEV_ONLY', 'index.ts')], 20 | 21 | mode: 'development', 22 | 23 | module: { 24 | rules: [ 25 | { 26 | include: [ 27 | path.resolve(ROOT, 'src'), 28 | path.resolve(ROOT, 'DEV_ONLY'), 29 | ], 30 | loader: 'babel-loader', 31 | options: { 32 | cacheDirectory: true, 33 | plugins: ['@babel/plugin-proposal-class-properties'], 34 | presets: ['@babel/preset-react'], 35 | }, 36 | test: /\.(js|ts|tsx)$/, 37 | }, 38 | ], 39 | }, 40 | 41 | output: { 42 | filename: 'moize.js', 43 | library: 'moize', 44 | libraryTarget: 'umd', 45 | path: path.resolve(ROOT, 'dist'), 46 | publicPath: `http://localhost:${PORT}/`, 47 | umdNamedDefine: true, 48 | }, 49 | 50 | plugins: [ 51 | new webpack.EnvironmentPlugin(['NODE_ENV']), 52 | new HtmlWebpackPlugin(), 53 | new ESLintWebpackPlugin(), 54 | ], 55 | 56 | resolve: { 57 | extensions: ['.tsx', '.ts', '.js'], 58 | }, 59 | }; 60 | --------------------------------------------------------------------------------