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 🙏

© 2026 – Pkg Stats / Ryan Hefner

@yuhere/js-functools

v1.0.5

Published

Function memoization library with pluggable store backends.

Readme

@yuhere/js-functools

Function memoization with pluggable store backends.
Works in Node.js and modern browsers.

npm install @yuhere/js-functools

Quick start

import { memoize, memoizeWithHit, remember } from "@yuhere/js-functools";

// Basic memoization — returns the result directly
const twice = memoize(async (x: number) => x * 2);
await twice(5);  // computes → 10
await twice(5);  // from store → 10

// With hit indicator — returns [hit, result]
const add = memoizeWithHit(async (x: number) => x + 10);
const [hit, result] = await add(5);
// hit = false (first call), result = 15
const [hit2, result2] = await add(5);
// hit2 = true (from store), result2 = 15

// Bypass memoized explicitly
const forced = await add.force(5);
// forced = 15 (always executes original function)

// One-shot memoization — stores the promise permanently
const getUser = remember(async (id: string) => fetch(`/api/users/${id}`));
const getter = getUser("abc123");
await getter(); // fetches
await getter(); // returns stored promise (no network call)

Options

memoize(fn, {
    name: "myFn",              // store namespace (default: fn.name or auto-generated)
    Store: MemStore,            // store constructor (default: MemStore)
    maxAge: 60_000,             // TTL in ms (default: 10 days, clamped 0–365 days)
    evict: (item) => false,     // custom eviction — return true to re-fetch
    returnNullOnError: (err, args) => false, // return true to suppress errors
});

maxAge

// Expire after 5 seconds
const fn = memoize(expensiveWork, { maxAge: 5_000 });

evict

// Re-fetch when the stored result is under 100
const fn = memoize(compute, {
    evict: (item) => item.result < 100,
});

// Async eviction — check an external condition
const fn2 = memoize(fetchData, {
    evict: async (item) => await isStale(item.hash),
});

returnNullOnError

Only errors thrown by your memoized function are eligible for suppression. Store read/write errors are still thrown.

// Silently return null instead of throwing
const fn = memoize(fragileApi, {
    returnNullOnError: (error, args) => true,
});
const result = await fn("input"); // null instead of throwing

// Suppress only specific errors
const fn2 = memoize(fragileApi, {
    returnNullOnError: (error, args) => error.message.includes("timeout"),
});

Stores

All stores implement the same interface (AbstractMemoizedStore):

| Method | Description | |---|---| | get(hash) | Retrieve a stored item | | put(item) | Store an item (validates name match) | | list(offset?, limit?) | Paginated listing (default offset=0, limit=100) | | delete(hash) | Remove a single item | | clear() | Remove all items for this store's name |

MemStore (default)

In-memory Map — works everywhere.

import { memoize, MemStore } from "@yuhere/js-functools";

const fn = memoize(work, { Store: MemStore });

SessionStore / LocalStore (browser)

Backed by sessionStorage / localStorage. Browser only.

import { memoize } from "@yuhere/js-functools";
import { SessionStore, LocalStore } from "@yuhere/js-functools/lib/memoize/store/browser/index.js";

// Session-scoped (cleared when tab closes)
const fn = memoize(work, { Store: SessionStore });

// Persistent across sessions
const fn2 = memoize(work, { Store: LocalStore });

IdbStore (browser)

IndexedDB-backed. All instances share one database (functools). Browser only.

import { memoize } from "@yuhere/js-functools";
import { IdbStore } from "@yuhere/js-functools/lib/memoize/store/browser/index.js";

const fn = memoize(work, { Store: IdbStore });

FSStore (Node.js)

Filesystem-backed — one JSON file per memoized result. Node.js only.

import { memoize } from "@yuhere/js-functools";
import { createFSStore } from "@yuhere/js-functools/lib/memoize/store/node/index.js";

const FSStore = createFSStore({
    dir: "./cache/memoize",  // cache directory
    compress: true,           // gzip stored files
});

const fn = memoize(heavyWork, { Store: FSStore, name: "heavyWork" });
// Files stored as: ./cache/memoize/heavyWork/heavyWork-<hash>.json.gz

SqliteStore (Node.js)

SQLite-backed — uses the built-in node:sqlite module. Requires Node.js 24+.

import { memoize } from "@yuhere/js-functools";
import { createSqliteStore } from "@yuhere/js-functools/lib/memoize/store/node/index.js";

// In-memory database
const SqliteStore = createSqliteStore();
const fn = memoize(work, { Store: SqliteStore });

// Persistent database
const SqliteStore2 = createSqliteStore({
    dbpath: "./cache/memoize.db",
    table: "my_cache",       // custom table name (default: "functools_memoize")
});
const fn2 = memoize(work, { Store: SqliteStore2 });

Listing & clearing

Every memoized function exposes ls and clear:

const fn = memoize(async (x: number) => x * 2);
await fn(1);
await fn(2);
await fn(3);

// List all stored items
const { total, list, offset, limit } = await fn.ls();
console.log(total); // 3

// Iterate the AsyncGenerator
for await (const item of list) {
    console.log(item.args, item.result);
}

// Paginate
const page = await fn.ls(0, 10); // offset=0, limit=10

// Clear everything
await fn.clear();

Metadata

const fn = memoize(expensiveWork, { name: "customName" });

console.log(fn.fnname);   // "customName"
console.log(fn.actual);   // the original function
console.log(fn.force);    // force execution (bypass cache)

Deduplication

Concurrent calls with the same arguments share one invocation:

const fn = memoize(async (x: number) => {
    console.log("called");
    return x * 2;
});

// Three concurrent calls — "called" logs only once
const [a, b, c] = await Promise.all([fn(5), fn(5), fn(5)]);

Writing a custom store

Extend AbstractMemoizedStore and implement the five methods:

import { AbstractMemoizedStore } from "@yuhere/js-functools";

type WorkFn = (id: string) => Promise<{ id: string; value: number }>;

class MyStore extends AbstractMemoizedStore<WorkFn> {
    // ... implement get, put, list, delete, clear
}

const fn = memoize(work, { Store: MyStore });

API reference

memoizeWithHit(fn, options?)

Returns (...args) => Promise<[boolean, Result]> — hit is true when the result came from the store.

The returned function also includes:

  • force(...args) — run original function and write-through to store
  • actual, fnname, ls(offset?, limit?), clear()

memoize(fn, options?)

Like memoizeWithHit but returns (...args) => Promise<Result> directly.

The returned function also includes:

  • force(...args) — run original function and write-through to store
  • actual, fnname, ls(offset?, limit?), clear()

remember(fn, scope?)

Returns (...args) => () => Promise<Result> — stores the promise on first invocation and reuses it forever. Arguments are only used on the first call; subsequent calls ignore them.

MemoizeOptions

| Option | Type | Default | |---|---|---| | name | string | fn.name or auto-generated | | Store | store constructor | MemStore | | maxAge | number (ms) | 864_000_000 (10 days) | | evict | (item) => boolean \| Promise<boolean> | ({expires}) => expires < Date.now() | | returnNullOnError | (error, args) => boolean | () => false |

MemoizedItem

type AnyFunction = (...args: any[]) => any;

interface MemoizedItem<T extends AnyFunction> {
    hash: string;    // SHA1-based hash of [name, args]
    name: string;    // store namespace
    args: Parameters<T>;   // original arguments
    result: ReturnType<T>; // stored result
    ts: number;      // creation timestamp
    expires: number; // expiry timestamp (ts + maxAge)
}

ListResult

interface ListResult<T extends AnyFunction> {
    total: number;
    offset: number;
    limit: number;
    list: AsyncGenerator<MemoizedItem<T>>;
}

Use for await...of to read list items.


Development

npm install
npx playwright install chromium
npm test

License

MIT