@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-functoolsQuick 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.gzSqliteStore (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 storeactual,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 storeactual,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 testLicense
MIT
