Files
blog/node_modules/moize/src/index.ts
2025-07-16 16:30:56 +00:00

617 lines
15 KiB
TypeScript

import memoize from 'micro-memoize';
import { createMoizedComponent } from './component';
import { DEFAULT_OPTIONS } from './constants';
import { createMoizeInstance } from './instance';
import { getMaxAgeOptions } from './maxAge';
import {
createOnCacheOperation,
getIsEqual,
getIsMatchingKey,
getTransformKey,
} from './options';
import {
clearStats,
collectStats,
getDefaultProfileName,
getStats,
getStatsOptions,
statsCache,
} from './stats';
import { createRefreshableMoized } from './updateCacheForKey';
import { combine, compose, isMoized, mergeOptions, setName } from './utils';
import type {
Expiration,
IsEqual,
IsMatchingKey,
MicroMemoizeOptions,
Moize,
Moizeable,
Moized,
OnExpire,
Options,
Serialize,
TransformKey,
UpdateCacheForKey,
} from '../index.d';
/**
* @module moize
*/
/**
* @description
* memoize a function based its arguments passed, potentially improving runtime performance
*
* @example
* import moize from 'moize';
*
* // standard implementation
* const fn = (foo, bar) => `${foo} ${bar}`;
* const memoizedFn = moize(fn);
*
* // implementation with options
* const fn = async (id) => get(`http://foo.com/${id}`);
* const memoizedFn = moize(fn, {isPromise: true, maxSize: 5});
*
* // implementation with convenience methods
* const Foo = ({foo}) => <div>{foo}</div>;
* const MemoizedFoo = moize.react(Foo);
*
* @param fn the function to memoized, or a list of options when currying
* @param [options=DEFAULT_OPTIONS] the options to apply
* @returns the memoized function
*/
const moize: Moize = function <
MoizeableFn extends Moizeable,
PassedOptions extends Options<MoizeableFn>
>(fn: MoizeableFn | PassedOptions, passedOptions?: PassedOptions) {
type CombinedOptions = Omit<Options<MoizeableFn>, keyof PassedOptions> &
PassedOptions;
const options: Options<MoizeableFn> = passedOptions || DEFAULT_OPTIONS;
if (isMoized(fn)) {
const moizeable = fn.originalFunction as MoizeableFn;
const mergedOptions = mergeOptions(
fn.options,
options
) as CombinedOptions;
return moize<MoizeableFn, CombinedOptions>(moizeable, mergedOptions);
}
if (typeof fn === 'object') {
return function <
CurriedFn extends Moizeable,
CurriedOptions extends Options<CurriedFn>
>(
curriedFn: CurriedFn | CurriedOptions,
curriedOptions: CurriedOptions
) {
type CombinedCurriedOptions = Omit<
CombinedOptions,
keyof CurriedOptions
> &
CurriedOptions;
if (typeof curriedFn === 'function') {
const mergedOptions = mergeOptions(
fn as CombinedOptions,
curriedOptions
) as CombinedCurriedOptions;
return moize(curriedFn, mergedOptions);
}
const mergedOptions = mergeOptions(
fn as CombinedOptions,
curriedFn as CurriedOptions
);
return moize(mergedOptions);
};
}
if (options.isReact) {
return createMoizedComponent(moize, fn, options);
}
const coalescedOptions: Options<MoizeableFn> = {
...DEFAULT_OPTIONS,
...options,
maxAge:
typeof options.maxAge === 'number' && options.maxAge >= 0
? options.maxAge
: DEFAULT_OPTIONS.maxAge,
maxArgs:
typeof options.maxArgs === 'number' && options.maxArgs >= 0
? options.maxArgs
: DEFAULT_OPTIONS.maxArgs,
maxSize:
typeof options.maxSize === 'number' && options.maxSize >= 0
? options.maxSize
: DEFAULT_OPTIONS.maxSize,
profileName: options.profileName || getDefaultProfileName(fn),
};
const expirations: Array<Expiration> = [];
const {
matchesArg: equalsIgnored,
isDeepEqual: isDeepEqualIgnored,
isPromise,
isReact: isReactIgnored,
isSerialized: isSerialzedIgnored,
isShallowEqual: isShallowEqualIgnored,
matchesKey: matchesKeyIgnored,
maxAge: maxAgeIgnored,
maxArgs: maxArgsIgnored,
maxSize,
onCacheAdd,
onCacheChange,
onCacheHit,
onExpire: onExpireIgnored,
profileName: profileNameIgnored,
serializer: serializerIgnored,
updateCacheForKey,
transformArgs: transformArgsIgnored,
updateExpire: updateExpireIgnored,
...customOptions
} = coalescedOptions;
const isEqual = getIsEqual(coalescedOptions);
const isMatchingKey = getIsMatchingKey(coalescedOptions);
const maxAgeOptions = getMaxAgeOptions(
expirations,
coalescedOptions,
isEqual,
isMatchingKey
);
const statsOptions = getStatsOptions(coalescedOptions);
const transformKey = getTransformKey(coalescedOptions);
const microMemoizeOptions: MicroMemoizeOptions<MoizeableFn> = {
...customOptions,
isEqual,
isMatchingKey,
isPromise,
maxSize,
onCacheAdd: createOnCacheOperation(
combine(
onCacheAdd,
maxAgeOptions.onCacheAdd,
statsOptions.onCacheAdd
)
),
onCacheChange: createOnCacheOperation(onCacheChange),
onCacheHit: createOnCacheOperation(
combine(
onCacheHit,
maxAgeOptions.onCacheHit,
statsOptions.onCacheHit
)
),
transformKey,
};
const memoized = memoize(fn, microMemoizeOptions);
let moized = createMoizeInstance<MoizeableFn, CombinedOptions>(memoized, {
expirations,
options: coalescedOptions,
originalFunction: fn,
});
if (updateCacheForKey) {
moized = createRefreshableMoized<typeof moized>(moized);
}
setName(moized, (fn as Moizeable).name, options.profileName);
return moized;
};
/**
* @function
* @name clearStats
* @memberof module:moize
* @alias moize.clearStats
*
* @description
* clear all existing stats stored
*/
moize.clearStats = clearStats;
/**
* @function
* @name collectStats
* @memberof module:moize
* @alias moize.collectStats
*
* @description
* start collecting statistics
*/
moize.collectStats = collectStats;
/**
* @function
* @name compose
* @memberof module:moize
* @alias moize.compose
*
* @description
* method to compose moized methods and return a single moized function
*
* @param moized the functions to compose
* @returns the composed function
*/
moize.compose = function (...moized: Moize[]) {
return compose<Moize>(...moized) || moize;
};
/**
* @function
* @name deep
* @memberof module:moize
* @alias moize.deep
*
* @description
* should deep equality check be used
*
* @returns the moizer function
*/
moize.deep = moize({ isDeepEqual: true });
/**
* @function
* @name getStats
* @memberof module:moize
* @alias moize.getStats
*
* @description
* get the statistics of a given profile, or overall usage
*
* @returns statistics for a given profile or overall usage
*/
moize.getStats = getStats;
/**
* @function
* @name infinite
* @memberof module:moize
* @alias moize.infinite
*
* @description
* a moized method that will remove all limits from the cache size
*
* @returns the moizer function
*/
moize.infinite = moize({ maxSize: Infinity });
/**
* @function
* @name isCollectingStats
* @memberof module:moize
* @alias moize.isCollectingStats
*
* @description
* are stats being collected
*
* @returns are stats being collected
*/
moize.isCollectingStats = function isCollectingStats(): boolean {
return statsCache.isCollectingStats;
};
/**
* @function
* @name isMoized
* @memberof module:moize
* @alias moize.isMoized
*
* @description
* is the fn passed a moized function
*
* @param fn the object to test
* @returns is fn a moized function
*/
moize.isMoized = function isMoized(fn: any): fn is Moized {
return typeof fn === 'function' && !!fn.isMoized;
};
/**
* @function
* @name matchesArg
* @memberof module:moize
* @alias moize.matchesArg
*
* @description
* a moized method where the arg matching method is the custom one passed
*
* @param keyMatcher the method to compare against those in cache
* @returns the moizer function
*/
moize.matchesArg = function (argMatcher: IsEqual) {
return moize({ matchesArg: argMatcher });
};
/**
* @function
* @name matchesKey
* @memberof module:moize
* @alias moize.matchesKey
*
* @description
* a moized method where the key matching method is the custom one passed
*
* @param keyMatcher the method to compare against those in cache
* @returns the moizer function
*/
moize.matchesKey = function (keyMatcher: IsMatchingKey) {
return moize({ matchesKey: keyMatcher });
};
function maxAge<MaxAge extends number>(
maxAge: MaxAge
): Moize<{ maxAge: MaxAge }>;
function maxAge<MaxAge extends number, UpdateExpire extends boolean>(
maxAge: MaxAge,
expireOptions: UpdateExpire
): Moize<{ maxAge: MaxAge; updateExpire: UpdateExpire }>;
function maxAge<MaxAge extends number, ExpireHandler extends OnExpire>(
maxAge: MaxAge,
expireOptions: ExpireHandler
): Moize<{ maxAge: MaxAge; onExpire: ExpireHandler }>;
function maxAge<
MaxAge extends number,
ExpireHandler extends OnExpire,
ExpireOptions extends {
onExpire: ExpireHandler;
}
>(
maxAge: MaxAge,
expireOptions: ExpireOptions
): Moize<{ maxAge: MaxAge; onExpire: ExpireOptions['onExpire'] }>;
function maxAge<
MaxAge extends number,
UpdateExpire extends boolean,
ExpireOptions extends {
updateExpire: UpdateExpire;
}
>(
maxAge: MaxAge,
expireOptions: ExpireOptions
): Moize<{ maxAge: MaxAge; updateExpire: UpdateExpire }>;
function maxAge<
MaxAge extends number,
ExpireHandler extends OnExpire,
UpdateExpire extends boolean,
ExpireOptions extends {
onExpire: ExpireHandler;
updateExpire: UpdateExpire;
}
>(
maxAge: MaxAge,
expireOptions: ExpireOptions
): Moize<{
maxAge: MaxAge;
onExpire: ExpireHandler;
updateExpire: UpdateExpire;
}>;
function maxAge<
MaxAge extends number,
ExpireHandler extends OnExpire,
UpdateExpire extends boolean,
ExpireOptions extends {
onExpire?: ExpireHandler;
updateExpire?: UpdateExpire;
}
>(
maxAge: MaxAge,
expireOptions?: ExpireHandler | UpdateExpire | ExpireOptions
) {
if (expireOptions === true) {
return moize({
maxAge,
updateExpire: expireOptions,
});
}
if (typeof expireOptions === 'object') {
const { onExpire, updateExpire } = expireOptions;
return moize({
maxAge,
onExpire,
updateExpire,
});
}
if (typeof expireOptions === 'function') {
return moize({
maxAge,
onExpire: expireOptions,
updateExpire: true,
});
}
return moize({ maxAge });
}
/**
* @function
* @name maxAge
* @memberof module:moize
* @alias moize.maxAge
*
* @description
* a moized method where the age of the cache is limited to the number of milliseconds passed
*
* @param maxAge the TTL of the value in cache
* @returns the moizer function
*/
moize.maxAge = maxAge;
/**
* @function
* @name maxArgs
* @memberof module:moize
* @alias moize.maxArgs
*
* @description
* a moized method where the number of arguments used for determining cache is limited to the value passed
*
* @param maxArgs the number of args to base the key on
* @returns the moizer function
*/
moize.maxArgs = function maxArgs(maxArgs: number) {
return moize({ maxArgs });
};
/**
* @function
* @name maxSize
* @memberof module:moize
* @alias moize.maxSize
*
* @description
* a moized method where the total size of the cache is limited to the value passed
*
* @param maxSize the maximum size of the cache
* @returns the moizer function
*/
moize.maxSize = function maxSize(maxSize: number) {
return moize({ maxSize });
};
/**
* @function
* @name profile
* @memberof module:moize
* @alias moize.profile
*
* @description
* a moized method with a profile name
*
* @returns the moizer function
*/
moize.profile = function (profileName: string) {
return moize({ profileName });
};
/**
* @function
* @name promise
* @memberof module:moize
* @alias moize.promise
*
* @description
* a moized method specific to caching resolved promise / async values
*
* @returns the moizer function
*/
moize.promise = moize({
isPromise: true,
updateExpire: true,
});
/**
* @function
* @name react
* @memberof module:moize
* @alias moize.react
*
* @description
* a moized method specific to caching React element values
*
* @returns the moizer function
*/
moize.react = moize({ isReact: true });
/**
* @function
* @name serialize
* @memberof module:moize
* @alias moize.serialize
*
* @description
* a moized method that will serialize the arguments passed to use as the cache key
*
* @returns the moizer function
*/
moize.serialize = moize({ isSerialized: true });
/**
* @function
* @name serializeWith
* @memberof module:moize
* @alias moize.serializeWith
*
* @description
* a moized method that will serialize the arguments passed to use as the cache key
* based on the serializer passed
*
* @returns the moizer function
*/
moize.serializeWith = function (serializer: Serialize) {
return moize({ isSerialized: true, serializer });
};
/**
* @function
* @name shallow
* @memberof module:moize
* @alias moize.shallow
*
* @description
* should shallow equality check be used
*
* @returns the moizer function
*/
moize.shallow = moize({ isShallowEqual: true });
/**
* @function
* @name transformArgs
* @memberof module:moize
* @alias moize.transformArgs
*
* @description
* transform the args to allow for specific cache key comparison
*
* @param transformArgs the args transformer
* @returns the moizer function
*/
moize.transformArgs = <Transformer extends TransformKey>(
transformArgs: Transformer
) => moize({ transformArgs });
/**
* @function
* @name updateCacheForKey
* @memberof module:moize
* @alias moize.updateCacheForKey
*
* @description
* update the cache for a given key when the method passed returns truthy
*
* @param updateCacheForKey the method to determine when to update cache
* @returns the moizer function
*/
moize.updateCacheForKey = <UpdateWhen extends UpdateCacheForKey>(
updateCacheForKey: UpdateWhen
) => moize({ updateCacheForKey });
// Add self-referring `default` property for edge-case cross-compatibility of mixed ESM/CommonJS usage.
// This property is frozen and non-enumerable to avoid visibility on iteration or accidental overrides.
Object.defineProperty(moize, 'default', {
configurable: false,
enumerable: false,
value: moize,
writable: false,
});
export default moize;