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 🙏

© 2024 – Pkg Stats / Ryan Hefner

memoize-utils

v1.0.1

Published

Memoization function and decorator.

Downloads

1,049

Readme

memoize-utils

npm

Memoize sync and async functions (Returning a Promise).

Cache expensive function calls and return the cached result when the same inputs occur again.

This package provides:

  • memoize Function: Used to memoize any sync or async function.
  • memoize Decorator: TypeScript decorator used to memoize class methods and getters.

Can be used to:

  • Cache expensive function calls
  • Prevent hitting rate limits on an API when the result can be cached
  • Speed up programs and prevent unnecessary computations and bandwidth usage

Installation

$ npm i memoize-utils

Usage

Memoizing a function:

import { memoize } from 'memoize-utils';

// Works with sync and async functions
function expensiveFunction() {
  // Some expensive operation that we want to cache its result
  return result;
}

const memoized = memoize(expensiveFunction);

memoized();
memoized(); // Returns cached result

Example with cache expiration:

import { memoize } from 'memoize-utils';

async function fetchIP() {
  const response = await fetch('http://httpbin.org/ip');
  return response.json();
}

const memoized = memoize(fetchIP, { maxAge: 2000 }); // Expires after 2 seconds

// The first request is cached
await memoized();

// Subsequent calls return the cached result
await memoized();

// Delay 2 seconds
await new Promise((resolve) => setTimeout(resolve, 2000));

// Cache has expired, make a new request and cache the result
await memoized();

Memoizing class methods and getters:

import { memoize } from 'memoize-utils/decorator';

class ExampleClass {
  @memoize({ maxAge: 2000 })
  async fetchIP() {
    const response = await fetch('http://httpbin.org/ip');
    return response.json();
  }

  @memoize({ maxAge: 60 * 60 * 1000 })
  get result() {
    // ...
  }
}

const instance = new ExampleClass();

// First call is cached and subsequent calls return the result from the cache until expiration
await instance.fetchIP();
await instance.fetchIP(); // Cached

// First access is cached, any access later returns the cached result until expiration
instance.result;

Options

| Option | Description | Default | | ---------------------- | ----------------------------------------------------------------------------------------------------------------- | ----------- | | maxAge | Cached results expiration duration in milliseconds (Defaults to no expiration). | undefined | | cache | Custom cache instance or a factory function returning a cache instance. | new Map() | | cacheId | Custom cache ID function, to be used to determine the ID of the cached result (Defaults to first argument as ID). | undefined | | cacheRejectedPromise | Cache the rejected promise when memoizing an async function. | false | | cacheFromContext | Function returning a custom cache instance that has access to the original function's context this. | undefined |

To customize these defaults, you can create a wrapper function:

import { memoize as memoizeFn } from 'memoize-utils';

export function memoize(fn, options) {
  const defaults = {
    maxAge: 60 * 60 * 1000, // Cache expires in 1 hour
    cache: new LRUCache(), // Use a custom cache instance
    cacheId: (obj) => obj.id, // Use a specific ID assuming your first arg is an object
    // ...
  };

  return memoizeFn(fn, { defaults, ...options });
}

// Use the new wrapper function
const memoized = memoize(expensiveFunction);

Cache Expiration

Cached results are stored with a timestamp, the maxAge option can be passed when creating the memoized function to set the expiration duration of the cache. The cache expiration is checked when the cache is accessed, so there are no timers that clear the cache automatically, if you need this functionality you can pass a custom cache object that supports it.

const memoized = memoize(expensiveFunction, { maxAge: 60 * 60 * 1000 }); // Cache results for 1 hour

Custom Cache

By default a Map() object is used to store the cached results, any object that implements a similar API can be used instead, for example WeakMap.

const cache = new WeakMap();

const memoized = memoize(expensiveFunction, { cache });

// We can also pass a factory function that returns a cache instance
const memoized = memoize(expensiveFunction, {
  cache: () => new WeakMap(),
});

Required cache object methods:

  • .set(key, value)
  • .get(key)
  • .has(key)
  • .delete(key)

Cache ID

By default the first argument of the memoized function is used as the cache ID to store the result.

const memoized = memoize(expensiveFunction);

// All of these 3 calls are considered the same since we're using the first argument as the cache ID
memoized('a');
memoized('a', 'b'); // Cached
memoized('a', 'b', 1); // Cached

To use all the arguments as the cache ID, we can pass a cacheId function:

function expensiveFunction(a, b, c) {
  // ...
}

const memoized = memoize(expensiveFunction, {
  // The cacheId function accepts the same arguments as the original function
  cacheId: (...args) => args.map(String).join('-'),
});

// In this case, each of these calls is cached separately
memoized('a');
memoized('a', 'b');
memoized('a', 'b', 1);

Object arguments require serialization, for example using JSON.stringify or any other serialization method.

function expensiveFunction(a = {}, b = null, c = true) {
  // ...
}

const memoized = memoize(expensiveFunction, {
  // Assuming all the arguments are JSON serializable
  cacheId: (...args) => JSON.stringify(args),
});

// Without serialization these calls wouldn't be considered the same
memoized({});
memoized({}); // Cached
memoized({}); // Cached

RegExp as arguments also must be serialized, for simplicity we can use RegExp.toString().

function expensiveFunction(a) {
  // ...
}

const memoized = memoize(expensiveFunction, {
  cacheId: (a) => a.toString(),
});

// Without serialization these calls wouldn't be considered the same
memoized(/(.*)/);
memoized(/(.*)/); // Cached
memoized(/(.*)/); // Cached

Cache ID Helpers

The module memoize-utils/helpers provides some commonly used cacheId functions:

  • all: Get an ID from all the arguments casted to a string and then joined together.
  • json: Get a JSON string ID from the arguments (JSON.stringify(args)).
  • anyOrder: Get the same ID from a set of arguments passed in any order.

Usage:

import { all, json, anyOrder } from 'memoize-utils/helpers';

// Use all the arguments as an ID
// Note: does not work with objects (Arguments are casted to strings)
// but it works with `RegExp` objects
memoize(fn, { cacheId: all });

// Use all the arguments as an ID including objects
// Note: does not work with `RegExp` objects
memoize(fn, { cacheId: json });

// Use all the arguments as an ID but in any order
// Note: does not work with objects (Arguments are casted to strings)
memoize(fn, { cacheId: anyOrder });

You can create your own memoize wrapper function using a custom cache ID:

import { memoize } from 'memoize-utils';
import { anyOrder } from 'memoize-utils/helpers';

export function memoizeAnyOrder(fn, options) {
  return memoize(fn, { cacheId: anyOrder, ...options });
}

// Memoize functions using all of the arguments as a cache ID in any order
const memoized = memoizeAnyOrder(fn);

Rejected Promises

By default rejected promises are not cached, this is done to have the same functionality for synchronous functions when throwing errors. If you want to also cache rejected promises, you can use the cacheRejectedPromise option.

// Cache rejected promises
// You might want to use it with `maxAge` so the result expires at some point and the original function call again
const memoized = memoize(expensiveFunction, { cacheRejectedPromise: true });

Cache From Context

If your cache instance requires the original function's context (this), you can use cacheFromContext function that has access to the same context as the original function and return a cache instance. For example this function is used to implement the decorator which uses a separate cache for each class instance.

function cacheFromContext() {
  // You have acces to the original function's context
  if (!this.cache) {
    this.cache = new Map();
  }

  return this.cache;
}

const memoized = memoize(expensiveFunction, { cacheFromContext });

Decorator Support

The moduleResolution config option must be set to node16 to be able to import the decorator.

import { memoize } from 'memoize-utils/decorator';

To enable support for decorators in your TypeScript project:

  • The experimentalDecorators TypeScript config must be set to true in the tsconfig.json file:
{
  "compilerOptions": {
    "experimentalDecorators": true
  }
}
  • Or using the command line option tsc --experimentalDecorators