diff --git a/.github/funding.yml b/.github/funding.yml index 10ed145..b5f4cea 100644 --- a/.github/funding.yml +++ b/.github/funding.yml @@ -1,2 +1 @@ -github: [sindresorhus, bfred-it] -tidelift: npm/mem +github: [sindresorhus, fregante] diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 3b8aa86..d9ff47b 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -10,12 +10,11 @@ jobs: fail-fast: false matrix: node-version: - - 16 - - 14 - - 12 + #- 20 + - 18 steps: - - uses: actions/checkout@v2 - - uses: actions/setup-node@v2 + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} - run: npm install diff --git a/index.ts b/index.ts index f1b4366..2826a2d 100644 --- a/index.ts +++ b/index.ts @@ -1,27 +1,27 @@ -import mimicFn from 'mimic-fn'; -import mapAgeCleaner from 'map-age-cleaner'; +import mimicFunction from 'mimic-function'; -type AnyFunction = (...arguments_: any) => any; +type AnyFunction = (...arguments_: readonly any[]) => unknown; const cacheStore = new WeakMap>(); +const cacheTimerStore = new WeakMap>(); -interface CacheStorageContent { +type CacheStorageContent = { data: ValueType; maxAge: number; -} +}; -interface CacheStorage { +type CacheStorage = { has: (key: KeyType) => boolean; get: (key: KeyType) => CacheStorageContent | undefined; set: (key: KeyType, value: CacheStorageContent) => void; delete: (key: KeyType) => void; clear?: () => void; -} +}; -export interface Options< +export type Options< FunctionToMemoize extends AnyFunction, CacheKeyType, -> { +> = { /** Milliseconds until the cache expires. @@ -37,18 +37,18 @@ export interface Options< You can have it cache **all** the arguments by value with `JSON.stringify`, if they are compatible: ``` - import mem from 'mem'; + import memoize from 'memoize'; - mem(function_, {cacheKey: JSON.stringify}); + memoize(function_, {cacheKey: JSON.stringify}); ``` Or you can use a more full-featured serializer like [serialize-javascript](https://github.com/yahoo/serialize-javascript) to add support for `RegExp`, `Date` and so on. ``` - import mem from 'mem'; + import memoize from 'memoize'; import serializeJavascript from 'serialize-javascript'; - mem(function_, {cacheKey: serializeJavascript}); + memoize(function_, {cacheKey: serializeJavascript}); ``` @default arguments_ => arguments_[0] @@ -63,20 +63,20 @@ export interface Options< @example new WeakMap() */ readonly cache?: CacheStorage>; -} +}; /** [Memoize](https://en.wikipedia.org/wiki/Memoization) functions - An optimization used to speed up consecutive function calls by caching the result of calls with identical input. -@param fn - Function to be memoized. +@param fn - The function to be memoized. @example ``` -import mem from 'mem'; +import memoize from 'memoize'; let index = 0; const counter = () => ++index; -const memoized = mem(counter); +const memoized = memoize(counter); memoized('foo'); //=> 1 @@ -93,7 +93,7 @@ memoized('bar'); //=> 2 ``` */ -export default function mem< +export default function memoize< FunctionToMemoize extends AnyFunction, CacheKeyType, >( @@ -104,8 +104,19 @@ export default function mem< maxAge, }: Options = {}, ): FunctionToMemoize { + if (maxAge === 0) { + return fn; + } + if (typeof maxAge === 'number') { - mapAgeCleaner(cache as unknown as Map>); + const maxSetIntervalValue = 2_147_483_647; + if (maxAge > maxSetIntervalValue) { + throw new TypeError(`The \`maxAge\` option cannot exceed ${maxSetIntervalValue}.`); + } + + if (maxAge < 0) { + throw new TypeError('The `maxAge` option should not be a negative number.'); + } } const memoized = function (this: any, ...arguments_: Parameters): ReturnType { @@ -113,7 +124,7 @@ export default function mem< const cacheItem = cache.get(key); if (cacheItem) { - return cacheItem.data; // eslint-disable-line @typescript-eslint/no-unsafe-return + return cacheItem.data; } const result = fn.apply(this, arguments_) as ReturnType; @@ -123,10 +134,22 @@ export default function mem< maxAge: maxAge ? Date.now() + maxAge : Number.POSITIVE_INFINITY, }); - return result; // eslint-disable-line @typescript-eslint/no-unsafe-return + if (typeof maxAge === 'number' && maxAge !== Number.POSITIVE_INFINITY) { + const timer = setTimeout(() => { + cache.delete(key); + }, maxAge); + + timer.unref?.(); + + const timers = cacheTimerStore.get(fn) ?? new Set(); + timers.add(timer as unknown as number); + cacheTimerStore.set(fn, timers); + } + + return result; } as FunctionToMemoize; - mimicFn(memoized, fn, { + mimicFunction(memoized, fn, { ignoreNonConfigurable: true, }); @@ -140,12 +163,12 @@ export default function mem< @example ``` -import {memDecorator} from 'mem'; +import {memoizeDecorator} from 'memoize'; class Example { index = 0 - @memDecorator() + @memoizeDecorator() counter() { return ++this.index; } @@ -154,14 +177,14 @@ class Example { class ExampleWithOptions { index = 0 - @memDecorator({maxAge: 1000}) + @memoizeDecorator({maxAge: 1000}) counter() { return ++this.index; } } ``` */ -export function memDecorator< +export function memoizeDecorator< FunctionToMemoize extends AnyFunction, CacheKeyType, >( @@ -174,7 +197,7 @@ export function memDecorator< propertyKey: string, descriptor: PropertyDescriptor, ): void => { - const input = target[propertyKey]; // eslint-disable-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access + const input = target[propertyKey]; // eslint-disable-line @typescript-eslint/no-unsafe-assignment if (typeof input !== 'function') { throw new TypeError('The decorated value must be a function'); @@ -185,7 +208,7 @@ export function memDecorator< descriptor.get = function () { if (!instanceMap.has(this)) { - const value = mem(input, options) as FunctionToMemoize; + const value = memoize(input, options) as FunctionToMemoize; instanceMap.set(this, value); return value; } @@ -198,9 +221,9 @@ export function memDecorator< /** Clear all cached data of a memoized function. -@param fn - Memoized function. +@param fn - The memoized function. */ -export function memClear(fn: AnyFunction): void { +export function memoizeClear(fn: AnyFunction): void { const cache = cacheStore.get(fn); if (!cache) { throw new TypeError('Can\'t clear a function that was not memoized!'); @@ -211,4 +234,8 @@ export function memClear(fn: AnyFunction): void { } cache.clear(); + + for (const timer of cacheTimerStore.get(fn) ?? []) { + clearTimeout(timer); + } } diff --git a/package.json b/package.json index be83c59..768d0c9 100644 --- a/package.json +++ b/package.json @@ -1,26 +1,29 @@ { - "name": "mem", - "version": "9.0.2", + "name": "memoize", + "version": "10.0.0", "description": "Memoize functions - An optimization used to speed up consecutive function calls by caching the result of calls with identical input", "license": "MIT", - "repository": "sindresorhus/mem", - "funding": "https://github.com/sindresorhus/mem?sponsor=1", + "repository": "sindresorhus/memoize", + "funding": "https://github.com/sindresorhus/memoize?sponsor=1", "author": { "name": "Sindre Sorhus", "email": "sindresorhus@gmail.com", "url": "https://sindresorhus.com" }, "type": "module", - "exports": "./dist/index.js", + "exports": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + }, + "sideEffects": false, "engines": { - "node": ">=12.20" + "node": ">=18" }, "scripts": { - "test": "xo && ava && npm run build && tsd", + "test": "xo && ava && npm run build && tsd --typings dist/index.d.ts", "build": "del-cli dist && tsc", "prepack": "npm run build" }, - "types": "dist/index.d.ts", "files": [ "dist" ], @@ -38,39 +41,31 @@ "promise" ], "dependencies": { - "map-age-cleaner": "^0.1.3", - "mimic-fn": "^4.0.0" + "mimic-function": "^5.0.0" }, "devDependencies": { - "@ava/typescript": "^1.1.1", - "@sindresorhus/tsconfig": "^1.0.2", - "@types/serialize-javascript": "^4.0.0", - "ava": "^3.15.0", - "del-cli": "^3.0.1", - "delay": "^4.4.0", - "serialize-javascript": "^5.0.1", - "ts-node": "^10.1.0", - "tsd": "^0.13.1", - "typescript": "^4.3.5", - "xo": "^0.41.0" + "@sindresorhus/tsconfig": "^5.0.0", + "@types/serialize-javascript": "^5.0.4", + "ava": "^5.3.1", + "del-cli": "^5.1.0", + "delay": "^6.0.0", + "serialize-javascript": "^6.0.1", + "ts-node": "^10.9.1", + "tsd": "^0.29.0", + "xo": "^0.56.0" }, "ava": { "timeout": "1m", "extensions": { "ts": "module" }, - "nonSemVerExperiments": { - "configurableModuleFormat": true - }, "nodeArguments": [ "--loader=ts-node/esm" ] }, "xo": { "rules": { - "@typescript-eslint/member-ordering": "off", - "@typescript-eslint/no-var-requires": "off", - "@typescript-eslint/no-empty-function": "off" + "@typescript-eslint/no-unsafe-return": "off" } } } diff --git a/readme.md b/readme.md index ca0d771..cb98811 100644 --- a/readme.md +++ b/readme.md @@ -1,4 +1,4 @@ -# mem +# memoize > [Memoize](https://en.wikipedia.org/wiki/Memoization) functions - An optimization used to speed up consecutive function calls by caching the result of calls with identical input @@ -12,18 +12,18 @@ If you want to memoize Promise-returning functions (like `async` functions), you ## Install -``` -$ npm install mem +```sh +npm install memoize ``` ## Usage ```js -import mem from 'mem'; +import memoize from 'memoize'; let index = 0; const counter = () => ++index; -const memoized = mem(counter); +const memoized = memoize(counter); memoized('foo'); //=> 1 @@ -49,11 +49,11 @@ memoized('bar', 'foo'); But you might want to use [p-memoize](https://github.com/sindresorhus/p-memoize) for more Promise-specific behaviors. ```js -import mem from 'mem'; +import memoize from 'memoize'; let index = 0; const counter = async () => ++index; -const memoized = mem(counter); +const memoized = memoize(counter); console.log(await memoized()); //=> 1 @@ -64,21 +64,21 @@ console.log(await memoized()); ``` ```js -import mem from 'mem'; +import memoize from 'memoize'; import got from 'got'; import delay from 'delay'; -const memGot = mem(got, {maxAge: 1000}); +const memoizedGot = memoize(got, {maxAge: 1000}); -await memGot('https://sindresorhus.com'); +await memoizedGot('https://sindresorhus.com'); // This call is cached -await memGot('https://sindresorhus.com'); +await memoizedGot('https://sindresorhus.com'); await delay(2000); // This call is not cached as the cache has expired -await memGot('https://sindresorhus.com'); +await memoizedGot('https://sindresorhus.com'); ``` ### Caching strategy @@ -86,7 +86,9 @@ await memGot('https://sindresorhus.com'); By default, only the first argument is compared via exact equality (`===`) to determine whether a call is identical. ```js -const power = mem((a, b) => Math.power(a, b)); +import memoize from 'memoize'; + +const power = memoize((a, b) => Math.power(a, b)); power(2, 2); // => 4, stored in cache with the key 2 (number) power(2, 3); // => 4, retrieved from cache at key 2 (number), it's wrong @@ -95,7 +97,9 @@ power(2, 3); // => 4, retrieved from cache at key 2 (number), it's wrong You will have to use the `cache` and `cacheKey` options appropriate to your function. In this specific case, the following could work: ```js -const power = mem((a, b) => Math.power(a, b), { +import memoize from 'memoize'; + +const power = memoize((a, b) => Math.power(a, b), { cacheKey: arguments_ => arguments_.join(',') }); @@ -110,7 +114,9 @@ More advanced examples follow. If your function accepts an object, it won't be memoized out of the box: ```js -const heavyMemoizedOperation = mem(heavyOperation); +import memoize from 'memoize'; + +const heavyMemoizedOperation = memoize(heavyOperation); heavyMemoizedOperation({full: true}); // Stored in cache with the object as key heavyMemoizedOperation({full: true}); // Stored in cache with the object as key, again @@ -120,7 +126,9 @@ heavyMemoizedOperation({full: true}); // Stored in cache with the object as key, You might want to serialize or hash them, for example using `JSON.stringify` or something like [serialize-javascript](https://github.com/yahoo/serialize-javascript), which can also serialize `RegExp`, `Date` and so on. ```js -const heavyMemoizedOperation = mem(heavyOperation, {cacheKey: JSON.stringify}); +import memoize from 'memoize'; + +const heavyMemoizedOperation = memoize(heavyOperation, {cacheKey: JSON.stringify}); heavyMemoizedOperation({full: true}); // Stored in cache with the key '[{"full":true}]' (string) heavyMemoizedOperation({full: true}); // Retrieved from cache @@ -129,7 +137,9 @@ heavyMemoizedOperation({full: true}); // Retrieved from cache The same solution also works if it accepts multiple serializable objects: ```js -const heavyMemoizedOperation = mem(heavyOperation, {cacheKey: JSON.stringify}); +import memoize from 'memoize'; + +const heavyMemoizedOperation = memoize(heavyOperation, {cacheKey: JSON.stringify}); heavyMemoizedOperation('hello', {full: true}); // Stored in cache with the key '["hello",{"full":true}]' (string) heavyMemoizedOperation('hello', {full: true}); // Retrieved from cache @@ -140,11 +150,12 @@ heavyMemoizedOperation('hello', {full: true}); // Retrieved from cache If your function accepts multiple arguments that aren't supported by `JSON.stringify` (e.g. DOM elements and functions), you can instead extend the initial exact equality (`===`) to work on multiple arguments using [`many-keys-map`](https://github.com/fregante/many-keys-map): ```js +import memoize from 'memoize'; import ManyKeysMap from 'many-keys-map'; const addListener = (emitter, eventName, listener) => emitter.on(eventName, listener); -const addOneListener = mem(addListener, { +const addOneListener = memoize(addListener, { cacheKey: arguments_ => arguments_, // Use *all* the arguments as key cache: new ManyKeysMap() // Correctly handles all the arguments for exact equality }); @@ -158,13 +169,13 @@ Better yet, if your function’s arguments are compatible with `WeakMap`, you sh ## API -### mem(fn, options?) +### memoize(fn, options?) #### fn Type: `Function` -Function to be memoized. +The function to be memoized. #### options @@ -212,15 +223,15 @@ Notes: Type: `object` -Same as options for `mem()`. +Same as options for `memoize()`. ```ts -import {memDecorator} from 'mem'; +import {memoizeDecorator} from 'memoize'; class Example { index = 0 - @memDecorator() + @memoizeDecorator() counter() { return ++this.index; } @@ -229,14 +240,14 @@ class Example { class ExampleWithOptions { index = 0 - @memDecorator({maxAge: 1000}) + @memoizeDecorator({maxAge: 1000}) counter() { return ++this.index; } } ``` -### memClear(fn) +### memoizeClear(fn) Clear all cached data of a memoized function. @@ -244,7 +255,7 @@ Clear all cached data of a memoized function. Type: `Function` -Memoized function. +The memoized function. ## Tips @@ -255,16 +266,16 @@ If you want to know how many times your cache had a hit or a miss, you can make #### Example ```js -import mem from 'mem'; +import memoize from 'memoize'; import StatsMap from 'stats-map'; import got from 'got'; const cache = new StatsMap(); -const memGot = mem(got, {cache}); +const memoizedGot = memoize(got, {cache}); -await memGot('https://sindresorhus.com'); -await memGot('https://sindresorhus.com'); -await memGot('https://sindresorhus.com'); +await memoizedGot('https://sindresorhus.com'); +await memoizedGot('https://sindresorhus.com'); +await memoizedGot('https://sindresorhus.com'); console.log(cache.stats); //=> {hits: 2, misses: 1} @@ -273,15 +284,3 @@ console.log(cache.stats); ## Related - [p-memoize](https://github.com/sindresorhus/p-memoize) - Memoize promise-returning & async functions - ---- - -
- - Get professional support for this package with a Tidelift subscription - -
- - Tidelift helps make open source sustainable for maintainers while giving companies
assurances about security, maintenance, and licensing for their dependencies. -
-
diff --git a/test-d/index.test-d.ts b/test-d/index.test-d.ts index e1b6f19..d043730 100644 --- a/test-d/index.test-d.ts +++ b/test-d/index.test-d.ts @@ -1,13 +1,14 @@ import {expectType} from 'tsd'; -import mem, {memClear} from '..'; +import memoize, {memoizeClear} from '../index.js'; +// eslint-disable-next-line unicorn/prefer-native-coercion-functions -- Required `string` type const fn = (text: string) => Boolean(text); -expectType(mem(fn)); -expectType(mem(fn, {maxAge: 1})); -expectType(mem(fn, {cacheKey: ([firstArgument]: [string]) => firstArgument})); +expectType(memoize(fn)); +expectType(memoize(fn, {maxAge: 1})); +expectType(memoize(fn, {cacheKey: ([firstArgument]: [string]) => firstArgument})); expectType( - mem(fn, { + memoize(fn, { // The cacheKey returns an array. This isn't deduplicated by a regular Map, but it's valid. The correct solution would be to use ManyKeysMap to deduplicate it correctly cacheKey: (arguments_: [string]) => arguments_, cache: new Map<[string], {data: boolean; maxAge: number}>(), @@ -15,7 +16,7 @@ expectType( ); expectType( // The `firstArgument` of `fn` is of type `string`, so it's used - mem(fn, {cache: new Map()}), + memoize(fn, {cache: new Map()}), ); /* Overloaded function tests */ @@ -25,34 +26,35 @@ function overloadedFn(parameter: boolean): boolean { return parameter; } -expectType(mem(overloadedFn)); -expectType(mem(overloadedFn)(true)); -expectType(mem(overloadedFn)(false)); +expectType(memoize(overloadedFn)); +expectType(memoize(overloadedFn)(true)); +expectType(memoize(overloadedFn)(false)); -memClear(fn); +memoizeClear(fn); // `cacheKey` tests. // The argument should match the memoized function’s parameters -mem((text: string) => Boolean(text), { - cacheKey: arguments_ => { +// eslint-disable-next-line unicorn/prefer-native-coercion-functions -- Required `string` type +memoize((text: string) => Boolean(text), { + cacheKey(arguments_) { expectType<[string]>(arguments_); }, }); -mem(() => 1, { - cacheKey: arguments_ => { +memoize(() => 1, { + cacheKey(arguments_) { expectType<[]>(arguments_); // eslint-disable-line @typescript-eslint/ban-types }, }); // Ensures that the various cache functions infer their arguments type from the return type of `cacheKey` -mem((_arguments: {key: string}) => 1, { - cacheKey: (arguments_: [{key: string}]) => { +memoize((_arguments: {key: string}) => 1, { + cacheKey(arguments_: [{key: string}]) { expectType<[{key: string}]>(arguments_); return new Date(); }, cache: { - get: key => { + get(key) { expectType(key); return { @@ -60,15 +62,15 @@ mem((_arguments: {key: string}) => 1, { maxAge: 2, }; }, - set: (key, data) => { + set(key, data) { expectType(key); expectType<{data: number; maxAge: number}>(data); }, - has: key => { + has(key) { expectType(key); return true; }, - delete: key => { + delete(key) { expectType(key); }, clear: () => undefined, diff --git a/test.ts b/test.ts index 23c68a8..e2e086c 100644 --- a/test.ts +++ b/test.ts @@ -1,59 +1,41 @@ import test from 'ava'; import delay from 'delay'; import serializeJavascript from 'serialize-javascript'; -import mem, {memDecorator, memClear} from './index.js'; +import memoize, {memoizeDecorator, memoizeClear} from './index.js'; test('memoize', t => { - let i = 0; - const fixture = () => i++; - const memoized = mem(fixture); + let index = 0; + const fixture = (a?: unknown, b?: unknown) => index++; + const memoized = memoize(fixture); t.is(memoized(), 0); t.is(memoized(), 0); t.is(memoized(), 0); - // @ts-expect-error t.is(memoized(undefined), 0); - // @ts-expect-error t.is(memoized(undefined), 0); - // @ts-expect-error t.is(memoized('foo'), 1); - // @ts-expect-error t.is(memoized('foo'), 1); - // @ts-expect-error t.is(memoized('foo'), 1); - // @ts-expect-error t.is(memoized('foo', 'bar'), 1); - // @ts-expect-error t.is(memoized('foo', 'bar'), 1); - // @ts-expect-error t.is(memoized('foo', 'bar'), 1); - // @ts-expect-error t.is(memoized(1), 2); - // @ts-expect-error t.is(memoized(1), 2); - // @ts-expect-error t.is(memoized(null), 3); - // @ts-expect-error t.is(memoized(null), 3); - // @ts-expect-error t.is(memoized(fixture), 4); - // @ts-expect-error t.is(memoized(fixture), 4); - // @ts-expect-error t.is(memoized(true), 5); - // @ts-expect-error t.is(memoized(true), 5); // Ensure that functions are stored by reference and not by "value" (e.g. their `.toString()` representation) - // @ts-expect-error - t.is(memoized(() => i++), 6); - // @ts-expect-error - t.is(memoized(() => i++), 7); + t.is(memoized(() => index++), 6); + t.is(memoized(() => index++), 7); }); test('cacheKey option', t => { - let i = 0; - const fixture = (..._arguments: any) => i++; - const memoized = mem(fixture, {cacheKey: ([firstArgument]) => String(firstArgument)}); + let index = 0; + const fixture = (..._arguments: any) => index++; + const memoized = memoize(fixture, {cacheKey: ([firstArgument]) => String(firstArgument)}); t.is(memoized(1), 0); t.is(memoized(1), 0); t.is(memoized('1'), 0); @@ -62,71 +44,55 @@ test('cacheKey option', t => { }); test('memoize with multiple non-primitive arguments', t => { - let i = 0; - const memoized = mem(() => i++, {cacheKey: JSON.stringify}); + let index = 0; + const memoized = memoize((a?: unknown, b?: unknown, c?: unknown) => index++, {cacheKey: JSON.stringify}); t.is(memoized(), 0); t.is(memoized(), 0); - // @ts-expect-error t.is(memoized({foo: true}, {bar: false}), 1); - // @ts-expect-error t.is(memoized({foo: true}, {bar: false}), 1); - // @ts-expect-error t.is(memoized({foo: true}, {bar: false}, {baz: true}), 2); - // @ts-expect-error t.is(memoized({foo: true}, {bar: false}, {baz: true}), 2); }); test('memoize with regexp arguments', t => { - let i = 0; - const memoized = mem(() => i++, {cacheKey: serializeJavascript}); + let index = 0; + const memoized = memoize((a?: unknown) => index++, {cacheKey: serializeJavascript}); t.is(memoized(), 0); t.is(memoized(), 0); - // @ts-expect-error t.is(memoized(/Sindre Sorhus/), 1); - // @ts-expect-error t.is(memoized(/Sindre Sorhus/), 1); - // @ts-expect-error t.is(memoized(/Elvin Peng/), 2); - // @ts-expect-error t.is(memoized(/Elvin Peng/), 2); }); test('memoize with Symbol arguments', t => { - let i = 0; + let index = 0; const argument1 = Symbol('fixture1'); const argument2 = Symbol('fixture2'); - const memoized = mem(() => i++); + const memoized = memoize((a?: unknown) => index++); t.is(memoized(), 0); t.is(memoized(), 0); - // @ts-expect-error t.is(memoized(argument1), 1); - // @ts-expect-error t.is(memoized(argument1), 1); - // @ts-expect-error t.is(memoized(argument2), 2); - // @ts-expect-error t.is(memoized(argument2), 2); }); test('maxAge option', async t => { - let i = 0; - const fixture = () => i++; - const memoized = mem(fixture, {maxAge: 100}); - // @ts-expect-error + let index = 0; + const fixture = (a?: unknown) => index++; + const memoized = memoize(fixture, {maxAge: 100}); t.is(memoized(1), 0); - // @ts-expect-error t.is(memoized(1), 0); await delay(50); - // @ts-expect-error t.is(memoized(1), 0); await delay(200); - // @ts-expect-error t.is(memoized(1), 1); }); test('maxAge option deletes old items', async t => { - let i = 0; - const fixture = () => i++; + let index = 0; + const fixture = (a?: unknown) => index++; const cache = new Map(); const deleted: number[] = []; const _delete = cache.delete.bind(cache); @@ -135,56 +101,47 @@ test('maxAge option deletes old items', async t => { return _delete(item); }; - // @ts-expect-error - const memoized = mem(fixture, {maxAge: 100, cache}); - // @ts-expect-error + const memoized = memoize(fixture, {maxAge: 100, cache}); t.is(memoized(1), 0); - // @ts-expect-error t.is(memoized(1), 0); t.is(cache.has(1), true); await delay(50); - // @ts-expect-error t.is(memoized(1), 0); t.is(deleted.length, 0); await delay(200); - // @ts-expect-error t.is(memoized(1), 1); t.is(deleted.length, 1); t.is(deleted[0], 1); }); test('maxAge items are deleted even if function throws', async t => { - let i = 0; - const fixture = () => { - if (i === 1) { + let index = 0; + const fixture = (a?: unknown) => { + if (index === 1) { throw new Error('failure'); } - return i++; + return index++; }; const cache = new Map(); - const memoized = mem(fixture, {maxAge: 100, cache}); - // @ts-expect-error + const memoized = memoize(fixture, {maxAge: 100, cache}); t.is(memoized(1), 0); - // @ts-expect-error t.is(memoized(1), 0); t.is(cache.size, 1); await delay(50); - // @ts-expect-error t.is(memoized(1), 0); await delay(200); t.throws(() => { - // @ts-expect-error memoized(1); }, {message: 'failure'}); t.is(cache.size, 0); }); test('cache option', t => { - let i = 0; - const fixture = (..._arguments: any) => i++; - const memoized = mem(fixture, { + let index = 0; + const fixture = (..._arguments: any) => index++; + const memoized = memoize(fixture, { cache: new WeakMap(), cacheKey: ([firstArgument]: [ReturnValue]): ReturnValue => firstArgument, }); @@ -197,25 +154,24 @@ test('cache option', t => { }); test('promise support', async t => { - let i = 0; - const memoized = mem(async () => i++); + let index = 0; + const memoized = memoize(async (a?: unknown) => index++); t.is(await memoized(), 0); t.is(await memoized(), 0); - // @ts-expect-error t.is(await memoized(10), 1); }); test('preserves the original function name', t => { - t.is(mem(function foo() {}).name, 'foo'); // eslint-disable-line func-names + t.is(memoize(function foo() {}).name, 'foo'); // eslint-disable-line func-names, @typescript-eslint/no-empty-function }); test('.clear()', t => { - let i = 0; - const fixture = () => i++; - const memoized = mem(fixture); + let index = 0; + const fixture = () => index++; + const memoized = memoize(fixture); t.is(memoized(), 0); t.is(memoized(), 0); - memClear(memoized); + memoizeClear(memoized); t.is(memoized(), 1); t.is(memoized(), 1); }); @@ -228,7 +184,7 @@ test('prototype support', t => { } } - Unicorn.prototype.foo = mem(Unicorn.prototype.foo); + Unicorn.prototype.foo = memoize(Unicorn.prototype.foo); const unicorn = new Unicorn(); @@ -242,12 +198,12 @@ test('.decorator()', t => { const returnValue2 = 101; class TestClass { - @memDecorator() + @memoizeDecorator() counter() { return returnValue++; } - @memDecorator() + @memoizeDecorator() counter2() { return returnValue2; } @@ -264,7 +220,7 @@ test('.decorator()', t => { test('memClear() throws when called with a plain function', t => { t.throws(() => { - memClear(() => {}); + memoizeClear(() => {}); // eslint-disable-line @typescript-eslint/no-empty-function }, { message: 'Can\'t clear a function that was not memoized!', instanceOf: TypeError, @@ -273,14 +229,113 @@ test('memClear() throws when called with a plain function', t => { test('memClear() throws when called on an unclearable cache', t => { const fixture = () => 1; - const memoized = mem(fixture, { + const memoized = memoize(fixture, { cache: new WeakMap(), }); t.throws(() => { - memClear(memoized); + memoizeClear(memoized); }, { message: 'The cache Map can\'t be cleared!', instanceOf: TypeError, }); }); + +test('maxAge - cache item expires after specified duration', async t => { + let index = 0; + const fixture = () => index++; + const memoized = memoize(fixture, {maxAge: 100}); + + t.is(memoized(), 0); // Initial call, cached + t.is(memoized(), 0); // Subsequent call, still cached + await delay(150); // Wait for longer than maxAge + t.is(memoized(), 1); // Cache expired, should compute again +}); + +test('maxAge - cache expiration timing is accurate', async t => { + let index = 0; + const fixture = () => index++; + const memoized = memoize(fixture, {maxAge: 100}); + + t.is(memoized(), 0); + await delay(90); // Wait for slightly less than maxAge + t.is(memoized(), 0); // Should still be cached + await delay(20); // Total delay now exceeds maxAge + t.is(memoized(), 1); // Should recompute as cache has expired +}); + +test('maxAge - expired items are not present in cache', async t => { + let index = 0; + const fixture = () => index++; + const cache = new Map(); + const memoized = memoize(fixture, {maxAge: 100, cache}); + + memoized(); // Call to cache the result + await delay(150); // Wait for cache to expire + memoized(); // Recompute and recache + t.is(cache.size, 1); // Only one item should be in the cache +}); + +test('maxAge - complex arguments and cache expiration', async t => { + let index = 0; + const fixture = object => index++; + const memoized = memoize(fixture, {maxAge: 100, cacheKey: JSON.stringify}); + + const arg = {key: 'value'}; + t.is(memoized(arg), 0); + await delay(150); + t.is(memoized(arg), 1); // Argument is the same, but should recompute due to expiration +}); + +test('maxAge - concurrent calls return cached value', async t => { + let index = 0; + const fixture = () => index++; + const memoized = memoize(fixture, {maxAge: 100}); + + t.is(memoized(), 0); + await delay(50); // Delay less than maxAge + t.is(memoized(), 0); // Should return cached value +}); + +test('maxAge - different arguments have separate expirations', async t => { + let index = 0; + const fixture = x => index++; + const memoized = memoize(fixture, {maxAge: 100}); + + t.is(memoized('a'), 0); + await delay(150); // Expire the cache for 'a' + t.is(memoized('b'), 1); // 'b' should be a separate cache entry + t.is(memoized('a'), 2); // 'a' should be recomputed +}); + +test('maxAge - zero maxAge means no caching', t => { + let index = 0; + const fixture = () => index++; + const memoized = memoize(fixture, {maxAge: 0}); + + t.is(memoized(), 0); + t.is(memoized(), 1); // No caching, should increment +}); + +test('maxAge - immediate expiration', async t => { + let index = 0; + const fixture = () => index++; + const memoized = memoize(fixture, {maxAge: 1}); + t.is(memoized(), 0); + await delay(10); + t.is(memoized(), 1); // Cache should expire immediately +}); + +test('maxAge - high concurrency', async t => { + let index = 0; + const fixture = () => index++; + const memoized = memoize(fixture, {maxAge: 50}); + + // Simulate concurrent calls + for (let job = 0; job < 10_000; job++) { + memoized(); + } + + await delay(100); + t.is(memoized(), 1); +}); diff --git a/tsconfig.json b/tsconfig.json index b6ac98e..b8dfe5b 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,5 +6,8 @@ }, "files": [ "index.ts" - ] + ], + "ts-node": { + "transpileOnly": true + } }