npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

@-xun/memoize

v1.1.0

Published

An extensible memoization cache and global singleton used to speed up expensive function calls

Downloads

251

Readme

Black Lives Matter! Last commit timestamp Codecov Source license Uses Semantic Release!

NPM version Monthly Downloads

memoize (@-xun/memoize)

An extremely flexible memoization cache and global singleton used to speed up expensive function calls.

Provides a simple but powerful API. Supports any number of parameters and/or a final "options" object parameter, asynchronous and synchronous functions, and per-function "scoped" caching. Provides nuanced usage statistics and super-powered TypeScript types for smooth DX.


Install

To install:

npm install @-xun/memoize

Usage

Original function without memoization:

function doExpensiveAnalysisOfFile(
  filePath: string,
  options: { activateFunctionality?: boolean } = {}
) {
  const { activateFunctionality } = options;
  const complexResult = expensiveAnalysis(filePath, activateFunctionality);

  return complexResult;
}

doExpensiveAnalysisOfFile('/repos/project/some-file.js', {
  activateFunctionality: true
});

memoize

memoize can be used to wrap an existing function with caching/memoization features.


Simple memoization:

import { memoize } from '@-xun/memoize';

//                                                vv memoized function
const memoizedDoExpensiveAnalysisOfFile = memoize(doExpensiveAnalysisOfFile);

//                                               vv cache id component(s)
const result = memoizedDoExpensiveAnalysisOfFile('/repos/project/some-file.js');
memoizedDoExpensiveAnalysisOfFile('/repos/project/some-file.js') === result; // true

Expiring memoization, where cache entries are evicted after a certain amount of time:

import { memoize } from '@-xun/memoize';

const memoizedDoExpensiveAnalysisOfFile = memoize(doExpensiveAnalysisOfFile, {
  maxAgeMs: 10_000
});

const result = memoizedDoExpensiveAnalysisOfFile('/repos/project/some-file.js');
memoizedDoExpensiveAnalysisOfFile('/repos/project/some-file.js') === result; // true

Memoization of an async function, optionally allowing the caller to explicitly recompute the cached value when desired:

import { memoize } from '@-xun/memoize';

// Suppose "asyncDoExpensiveAnalysisOfFile" is defined as an async version of
// "doExpensiveAnalysisOfFile"

const memoizedDoExpensiveAnalysisOfFile = memoize(
  asyncDoExpensiveAnalysisOfFile,
  { addUseCachedOption: true }
);

const result = await memoizedDoExpensiveAnalysisOfFile(
  '/repos/project/some-file.js',
  {
    // Will look in the cache for a result first (wrt the given filePath).
    useCached: true
  }
);

(await memoizedDoExpensiveAnalysisOfFile('/repos/project/some-file.js', {
  // Will look in the cache for a result first (wrt the given filePath).
  useCached: true
})) === result; // true

(await memoizedDoExpensiveAnalysisOfFile('/repos/project/some-file.js', {
  // Will bypass the cache and force recomputation, then cache the result.
  useCached: false
})) !== result; // true

memoizer

memoizer can be used to implement caching/memoization within a function itself.


Basic memoization:

import { memoizer } from '@-xun/memoize';

function doExpensiveAnalysisOfFile(
  filePath: string,
  options: { activateFunctionality: boolean }
): AnalysisResult {
  const { activateFunctionality } = options;

  //                               vv memoized function
  let complexResult = memoizer.get(doExpensiveAnalysisOfFile, [
    filePath, // <-- first cache id component
    options //   <-- second cache id component
  ]);

  if (complexResult === undefined) {
    complexResult = expensiveAnalysis(filePath, activateFunctionality);
    memoizer.set(doExpensiveAnalysisOfFile, [filePath, options], complexResult);
  }

  return complexResult;
}

const result = doExpensiveAnalysisOfFile('/repos/project/some-file.js', {
  activateFunctionality: true
});

doExpensiveAnalysisOfFile('/repos/project/some-file.js', {
  activateFunctionality: true
}) === result; // true

doExpensiveAnalysisOfFile('/repos/project/some-file.js', {
  activateFunctionality: false
}) !== result; // true

Optional memoization, allowing the caller to explicitly recompute the cached value when desired:

import { memoizer } from '@-xun/memoize';

// It is usually ideal to force the caller to acknowledge that they're dealing
// with a memoized function, which can prevent bad surprises. Still, we could
// have made useCached optional if we wanted to.

function doExpensiveAnalysisOfFile(
  filePath: string,
  {
    useCached,
    ...cacheIdComponents
  }: { activateFunctionality?: boolean; useCached: boolean }
): AnalysisResult {
  const { activateFunctionality } = cacheIdComponents;
  let complexResult;

  if (useCached) {
    complexResult = memoizer.get(doExpensiveAnalysisOfFile, [
      filePath,
      cacheIdComponents
    ]);
  }

  if (complexResult === undefined) {
    complexResult = expensiveAnalysis(filePath, activateFunctionality);

    memoizer.set(
      doExpensiveAnalysisOfFile,
      [filePath, cacheIdComponents],
      complexResult
    );
  }

  return complexResult;
}

const result = doExpensiveAnalysisOfFile('/repos/project/some-file.js', {
  // Will look in the cache for a result first (wrt the given filePath).
  useCached: true
});

doExpensiveAnalysisOfFile('/repos/project/some-file.js', {
  // Will look in the cache for a result first (wrt the given filePath).
  useCached: true
}) === result; // true

doExpensiveAnalysisOfFile('/repos/project/some-file.js', {
  // Will bypass the cache and force recomputation, then cache the result.
  useCached: false
}) !== result; // true

More complex memoization, where we accept an array of things with all, some, or none have been cached already. Our goal here is to do as little work as possible:

import { memoizer } from '@-xun/memoize';

function doExpensiveAnalysisOfFiles(
  filePaths: string[],
  {
    useCached = true,
    ...cacheIdComponents
  }: { activateFunctionality?: boolean; useCached?: boolean } = {}
): AnalysisResult[] {
  const { activateFunctionality } = cacheIdComponents;
  const complexResults = [];

  for (const filePath of filePaths) {
    let complexResult;

    if (useCached) {
      complexResult = memoizer.get<
        typeof doExpensiveAnalysisOfFiles,
        // DO "unpack" id components; go from `T | T[]` to `T` (this is the default).
        'expect unpacked ids',
        // DO "unpack" the return value; go from `T | T[]` to `T`.
        'expect unpacked value'
      >(doExpensiveAnalysisOfFiles, [filePath, cacheIdComponents]);
    }

    if (complexResult === undefined) {
      complexResult = expensiveAnalysis(filePath, activateFunctionality);

      memoizer.set<
        typeof doExpensiveAnalysisOfFiles,
        // DO "unpack" id components; go from `T | T[]` to `T` (this is the default).
        'expect unpacked ids',
        // DO "unpack" the return value; go from `T | T[]` to `T`.
        'expect unpacked value'
      >(
        doExpensiveAnalysisOfFiles,
        [filePath, cacheIdComponents],
        complexResult
      );
    }

    complexResults.push(complexResult);
  }

  return complexResults;
}

const result = doExpensiveAnalysisOfFiles([
  '/repos/project/some-file-1.js',
  '/repos/project/some-file-2.js',
  '/repos/project/some-file-3.js'
]);

// Even though the parameters are different, we can still take advantage of the
// memoized result of the previous invocation! No extra work is done by the
// following:
doExpensiveAnalysisOfFiles(['/repos/project/some-file-2.js'])[0] === result[1]; // true

More complex memoization, where we accept and memoize an array of things in one shot:

import { memoizer } from '@-xun/memoize';

function doExpensiveAnalysisOfFiles(
  filePaths: string[],
  {
    useCached,
    ...cacheIdComponents
  }: { activateFunctionality?: boolean; useCached: boolean }
): AnalysisResult[] {
  const { activateFunctionality } = cacheIdComponents;
  let complexResults;

  if (useCached) {
    complexResults = memoizer.get<
      typeof doExpensiveAnalysisOfFiles,
      // DO NOT "unpack" id components; leave them as they are.
      'expect ids as-is',
      // DO NOT "unpack" the return value; leave it as-is (this is the default).
      'expect value as-is'
    >(doExpensiveAnalysisOfFiles, [filePaths, cacheIdComponents]);
  }

  if (complexResults === undefined) {
    complexResults = expensiveAnalyses(filePaths, activateFunctionality);

    memoizer.set<
      typeof doExpensiveAnalysisOfFiles,
      // DO NOT "unpack" id components; leave them as they are.
      'expect ids as-is',
      // DO NOT "unpack" the return value; leave it as-is (this is the default).
      'expect value as-is'
    >(
      doExpensiveAnalysisOfFiles,
      [filePaths, cacheIdComponents],
      complexResults
    );
  }

  return complexResults;
}

const result = doExpensiveAnalysisOfFiles(
  [
    '/repos/project/some-file-1.js',
    '/repos/project/some-file-2.js',
    '/repos/project/some-file-3.js'
  ],
  {
    activateFunctionality: true,
    useCached: true
  }
);

doExpensiveAnalysisOfFiles(
  [
    '/repos/project/some-file-1.js',
    '/repos/project/some-file-2.js',
    '/repos/project/some-file-3.js'
  ],
  {
    // This being false means a different cache key is generated and the
    // previous results are not reused, even though filePaths (the other id
    // component) is the same!
    activateFunctionality: false,
    useCached: true
  }
) !== result; // true

Memoization of an async function using object-style parameters:

import { memoizer } from '@-xun/memoize';

async function doExpensiveAnalysisOfFile({
  useCached,
  ...cacheIdComponents
}: {
  filePath: string;
  activateFunctionality?: boolean;
  useCached: boolean;
}): Promise<AnalysisResult> {
  const { filePath, activateFunctionality } = cacheIdComponents;
  let complexResult: AnalysisResult | undefined;

  if (useCached) {
    // memoizer.get returns a promise iff its first parameter is detected to be
    // an async function or iff memoizer.set is called with `wasPromised: true`.
    complexResult = await memoizer.get(doExpensiveAnalysisOfFile, [
      cacheIdComponents
    ]);
  }

  if (complexResult === undefined) {
    // Do not put promises into the cache. Intellisense will attempt to stop you
    // from doing so. memoizer.get will return the value wrapped in a promise.
    complexResult = await expensiveAnalysis(filePath, activateFunctionality);
    memoizer.set(doExpensiveAnalysisOfFile, [cacheIdComponents], complexResult);
  }

  return complexResult;
}

const result = await doExpensiveAnalysisOfFile({
  filePath: '/repos/project/some-file.js',
  // Will look in the cache for a result first (wrt the given filePath).
  useCached: true
});

(await doExpensiveAnalysisOfFile({
  filePath: '/repos/project/some-file.js',
  // Will look in the cache for a result first (wrt the given filePath).
  useCached: true
})) === result; // true

(await doExpensiveAnalysisOfFile({
  filePath: '/repos/project/some-file.js',
  // Will bypass the cache and force recomputation, then cache the result.
  useCached: false
})) !== result; // true

Piecemeal memoization, where we customize which parameters are considered as components of the cache key and ignore the others:

import { memoizer } from '@-xun/memoize';

function doExpensiveAnalysisOfFile({
  useCached,
  activateFunctionality = true,
  // We only want to use a subset of options as the cache id components.
  ...cacheIdComponents
}: {
  filePath: string;
  activateFunctionality: boolean;
  activateOtherFunctionality?: boolean;
  somethingElse: number;
  useCached: boolean;
}): AnalysisResult {
  // We'll use this type to tell memoizer what id components to look out for.
  type MemoizedDoExpensiveAnalysisOfFile = (
    ...args: [typeof cacheIdComponents]
  ) => ReturnType<typeof doExpensiveAnalysisOfFile>;

  // These three properties will be used as components for our cache "id". If
  // one of them changes, the cache will miss. The other properties are ignored.
  const { filePath, activateOtherFunctionality, somethingElse } =
    cacheIdComponents;

  let complexResult;

  if (useCached) {
    complexResult = memoizer.get<MemoizedDoExpensiveAnalysisOfFile>(
      doExpensiveAnalysisOfFile as MemoizedDoExpensiveAnalysisOfFile,
      [cacheIdComponents]
    );
  }

  if (complexResult === undefined) {
    complexResult = expensiveAnalysis(
      filePath,
      activateFunctionality,
      activateOtherFunctionality
    );

    memoizer.set<MemoizedDoExpensiveAnalysisOfFile>(
      doExpensiveAnalysisOfFile as MemoizedDoExpensiveAnalysisOfFile,
      [cacheIdComponents],
      complexResult
    );
  }

  doSomethingElse(somethingElse);
  return complexResult;
}

const result = doExpensiveAnalysisOfFile({
  filePath: '/repos/project/some-file.js',
  useCached: true,
  activateFunctionality: true,
  somethingElse: 5
});

// Cache hit (despite activateFunctionality)
doExpensiveAnalysisOfFile({
  filePath: '/repos/project/some-file.js',
  useCached: true,
  activateFunctionality: false,
  somethingElse: 5
}) === result; // true

// Cache miss (despite activateFunctionality)
doExpensiveAnalysisOfFile({
  filePath: '/repos/project/some-file.js',
  useCached: true,
  activateFunctionality: true,
  somethingElse: 6
}) !== result; // true

Expiring cache entries (in this example: 10 seconds after being set unless set again), clearing the cache on a per-scope basis, and accessing cache usage metadata:

import { memoizer } from '@-xun/memoize';

async function doExpensiveAnalysisOfFile({
  useCached,
  ...cacheIdComponents
}: {
  filePath: string;
  activateFunctionality?: boolean;
  useCached: boolean;
}): Promise<AnalysisResult> {
  const { filePath, activateFunctionality } = cacheIdComponents;
  let complexResult: AnalysisResult | undefined;

  if (useCached) {
    complexResult = await memoizer.get(doExpensiveAnalysisOfFile, [
      cacheIdComponents
    ]);
  }

  if (complexResult === undefined) {
    complexResult = await expensiveAnalysis(filePath, activateFunctionality);
    memoizer.set(
      doExpensiveAnalysisOfFile,
      [cacheIdComponents],
      complexResult,
      { maxAgeMs: 10_000 }
    );
  }

  return complexResult;
}

const result = await doExpensiveAnalysisOfFile({
  filePath: '/repos/project/some-file.js',
  useCached: true
});

// Hits the cache
(await doExpensiveAnalysisOfFile({
  filePath: '/repos/project/some-file.js',
  useCached: true
})) === result;

// Clears the cache but only for the specified function
memoizer.clear([doExpensiveAnalysisOfFile]);

// Misses the cache
(await doExpensiveAnalysisOfFile({
  filePath: '/repos/project/some-file.js',
  useCached: true
})) !== result;

// If we waited 10 seconds and tried calling doExpensiveAnalysisOfFile again,
// we would miss the cache again, expirations (below) would be set to 1, and
// pendingExpirations (also below) would be set to 0.

// If we waited only 5 seconds before calling doExpensiveAnalysisOfFile again,
// we would hit the cache instead.

console.log(memoizer);

// {
//   set: [Function: setInCache],
//   sets: 2,
//   setsOverwrites: 0,
//   setsCreated: 2,
//   get: [Function: getFromCache],
//   gets: 3,
//   getsHits: 1,
//   getsMisses: 2,
//   clear: [Function: clearCacheByScope],
//   clearAll: [Function: clearCache],
//   clears: 1,
//   expirations: 0,
//   pendingExpirations: 1,
//   cachedScopes: 1,
//   cachedEntries: 1,
// }

Other Considerations

  • The internal cache is implemented as a global singleton that will persist across the entire runtime (but not cross-realm), even when imported from different packages. No need to worry about any of the usual package hazards.

  • The useCached property, if used as part of an "options" object, is omitted from the type of the secondary optional parameter. The name of this property can be customized, and additional properties can be similarly omitted, using the SecondaryKeysToOmit generic parameter on memoizer.get and memoizer.set.

  • The order of id components will change the derived cache key, resulting in a recomputation. If this is not desired, ensure id components are passed to memoizer's functions in consistently.

  • All id components passed to memoize and memoizer's functions must be serializable via JSON.stringify or explicitly undefined. If any id components are defined but not serializable, create a wrapper function that transforms any unserializable parameters into some serializable representation before passing them to memoize/memoizer.

[!CAUTION]

JSON.stringify will not consistently throw when it encounters unserializable or semi-serializable id components!

If used carelessly, this can lead to arbitrary cache key collisions where the memoizer functions return the same result for obviously different sets of function parameters when it clearly shouldn't.

To prevent this, ensure your function's memoized parameters (specifically the parameters used as id components) are serializable.

Appendix

Further documentation can be found under docs/.

Published Package Details

This is a CJS2 package with statically-analyzable exports built by Babel for use in Node.js versions that are not end-of-life. For TypeScript users, this package supports both "Node10" and "Node16" module resolution strategies.

That means both CJS2 (via require(...)) and ESM (via import { ... } from ... or await import(...)) source will load this package from the same entry points when using Node. This has several benefits, the foremost being: less code shipped/smaller package size, avoiding dual package hazard entirely, distributables are not packed/bundled/uglified, a drastically less complex build process, and CJS consumers aren't shafted.

Each entry point (i.e. ENTRY) in package.json's exports[ENTRY] object includes one or more export conditions. These entries may or may not include: an exports[ENTRY].types condition pointing to a type declaration file for TypeScript and IDEs, a exports[ENTRY].module condition pointing to (usually ESM) source for Webpack/Rollup, a exports[ENTRY].node and/or exports[ENTRY].default condition pointing to (usually CJS2) source for Node.js require/import and for browsers and other environments, and other conditions not enumerated here. Check the package.json file to see which export conditions are supported.

Note that, regardless of the { "type": "..." } specified in package.json, any JavaScript files written in ESM syntax (including distributables) will always have the .mjs extension. Note also that package.json may include the sideEffects key, which is almost always false for optimal tree shaking where appropriate.

License

See LICENSE.

Contributing and Support

New issues and pull requests are always welcome and greatly appreciated! 🤩 Just as well, you can star 🌟 this project to let me know you found it useful! ✊🏿 Or buy me a beer, I'd appreciate it. Thank you!

See CONTRIBUTING.md and SUPPORT.md for more information.

Contributors

All Contributors

Thanks goes to these wonderful people (emoji key):

This project follows the all-contributors specification. Contributions of any kind welcome!