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

extensor-cache

v1.1.4

Published

Simple caching library.

Downloads

99

Readme

extensor-cache-js

npm license

Extensor cache is a very simple, non-persistent application layer caching library, originally written for Extensor, which helps to improve resiliency when you're doing read/write over a network.

Extensor cache takes a callback and handles the caching and retry logic, so you don't have to switch on your brain. A lot of the examples here refer to HTTP requests, but you can use it for any form of IO, as long as there is an async function to handle it.

Out of the box, it can handle various read/write strategies:

  • read-through
  • read-around
  • write-through
  • write-back

See below for more about when and how to use those.


Table of contents

Usage

Install Extensor cache with your favourite package manager. I use npm cos I'm cool like that.

npm i extensor-cache

Instantiate an ExtensorCache object and configure a key pattern to start caching fetched and written values.

When fetching data, return it from an async callback to cache it:

import {
  ExtensorCache,
  InMemoryStore,
  KeyConfig,
} from "extensor-cache";

const cache = new ExtensorCache(new InMemoryStore());
const keyConfig = new KeyConfig("examples/{exampleName}/objects/{object}");
keyConfig.readCallback = async (context) => {
  return await someLongRunningFetch(context.params.exampleName, context.params.object);
}
cache.register(keyConfig);

const value = await cache.get("examples/readme/objects/usage");
console.log(value); // result of someLongRunningFetch("readme", "usage")

Key patterns

Key patterns idenfity cached variables. Extensor cache supports both static and dynamic key patterns. Key patterns can be used to identify values that are always fetched by the same callback. Dynamic key patterns contain parameters, whereas static patterns do not.

Static patterns

// ...
const config = new KeyConfig("examples/readme");
config.readCallback = async () => {
  return await someLongRunningFetch("https://example.com/some-fixed-endpoint");
}
cache.register(config);

Dynamic patterns

Parameters can be added to key patterns by wrapping them in curly brackets. The string inside the curly brackets is used to name the parameter. Parameters can be accessed inside the callback via the context object that is passed:

// ...
const config = new KeyConfig("examples/{exampleName}/objects/{object}");
config.readCallback = async (context) => {
  // here we have access to
  //   - context.params.exampleName
  //   - context.params.object
  return await someLongRunningFetch(
    context.params.exampleName, 
    context.params.object
  );
}
cache.register(config);

Error handling & propagation

Understanding how errors propagate helps you choose strategies and write robust handlers.

  • Read callbacks

    • ReadStrategies.readThrough: on a cache miss the readCallback is invoked; if it throws/rejects the error propagates to the caller of cache.get().
    • ReadStrategies.readAround: the callback is called first; if it resolves its value is used; if it rejects the cache will be consulted as a fallback (if available) — the rejection will propagate only if both the callback and cache miss fail.
    • ReadStrategies.cacheOnly: no callback is invoked; errors only occur for cache-layer problems (rare for in-memory stores).
  • Write callbacks

    • WriteStrategies.writeThrough: cache.set() waits for the writeCallback to succeed. If the callback throws/rejects, the write fails and the error propagates to the caller; the cache will not be updated.
    • WriteStrategies.writeBack: cache.set() updates the cache and returns immediately; the writeCallback runs in the background. Errors during background writes do not propagate to the original caller — they are retried according to the retry settings and will fail quietly when retry limits are exhausted.
    • WriteStrategies.cacheOnly: the writeCallback is not invoked; no remote side-effect occurs.
  • Evictions and updates

    • evictCallback and updateCallback share the configured write strategy and retry settings; their errors follow the same rules as write callbacks.

Best practice examples

// handle read errors (read-through)
try {
  const v = await cache.get("api/users/123");
  console.log(v);
} catch (err) {
  // handle network/read error (fallback, default value, or surface to caller)
  console.error("Failed to read user", err);
}

// write-through: caller will see write errors
try {
  await cache.set("db/products/456", { name: "x" }); // will throw if writeCallback fails
} catch (err) {
  console.error("Write failed and cache not updated:", err);
}

// write-back: caller will NOT see write errors; monitor retry events or logs instead
await cache.set("metrics/page_view/789", { page: "/products" });
// consider adding metrics/logging around background retries to observe failures

Retries

When using the write-behind strategy, writes and deletes to a key will be automatically retried depending on the config you pass. By default, retries are exponentially backed off with jitter, with intervals calculated from the base retry interval you pass as writeRetryInterval. This behaviour can be disabled or adjusted using the writeRetryBackoff and writeRetryIntervalCap configuration values.


Global Config

Global configuration that applies default for all keys can be set by passing an instance of GlobalConfig as the second argument to the constructor of ExtensorCache.

import {
  GlobalConfig,
  ExtensorCache,
  InMemoryStore,
  KeyConfig,
} from "extensor-cache";

const globalConfig = GlobalConfig();
globalConfig.ttl = 15 * 60;  // set the TTL of all keys to 15 mins.
const cache = new ExtensorCache(new InMemoryStore(), globalConfig);
const keyConfig = new KeyConfig("examples/{exampleName}/objects/{object}");
keyConfig.readCallback = async (context) => {
  return await someLongRunningFetch(context.params.exampleName, context.params.object);
}
cache.register(keyConfig);

const value = await cache.get("examples/readme/objects/usage");
console.log(value); // result of someLongRunningFetch("readme", "usage")

Configuration options

ttl

Default time in seconds for cache entries to live.

readStrategy

Default read strategy.

writeStrategy

Default write strategy.

writeRetryCount

Default number of times that a failed write should be retried. Only relevant if write strategy is write-behind.

writeRetryInterval

Default base time to wait before retrying a failed write. This is the time waited until the first retry is made. Further retries will be exponentially backed off (unlesss disabled). Only relevant if write strategy is write-behind.

writeRetryBackoff

When true, subsequent write retries will be exponentially backed off with jitter. Defaults to true. Only relevant if write strategy is write-behind.

writeRetryIntervalCap

The maximum time that should be waited between retries. Defaults to 1 hour. Only relevant if exponential backoff is enabled and the write strategy is write-behind.

Overriding global config

All global config values can be overridden at the individual key level using the KeyConfiguration object as shown below.


Key-Level Configuration

ttl
const config = new KeyConfig("examples/{exampleName}");
config.ttl = 600; // time in seconds for the cache entry to live
cache.register(config);
readCallback
const config = new KeyConfig("examples/{exampleName}");
config.readCallback = async (context) => {
  // the callback to run when a read is requested.
  // throw an error if the read was unsuccessful.
  // on success, this callback should return the value to be cached.
};
cache.register(config);
readStrategy
import { ReadStrategies } from "extensor-cache";

const config = new KeyConfig("examples/{exampleName}");
config.readStrategy = ReadStrategies.readThrough; 
// Tells the cache which read strategy to use for this pattern.
// Should be something imported from ReadStrategies.
// Any other value will cause cache-only to be used.
// Default is cache-only.
cache.register(config);
writeCallback
const config = new KeyConfig("examples/{exampleName}");
config.writeCallback = async (context) => {
  // the callback to run when a write is triggered.
  // throw an error if the write does not complete. 
  // if no error is thrown, the write will be considered successful.
  // this callback shouldn't return anything.

  // bear in mind that the error will not make it to the calling 
  // context if the  write strategy is write-back.
};
cache.register(config);
writeStrategy
import { WriteStrategies } from "extensor-cache";

const config = new KeyConfig("examples/{exampleName}");
config.writeStrategy = WriteStrategies.writeThrough; 
// Tells the cache which write strategy to use for this pattern.
// Should be something imported from WriteStrategies.
// Any other value will cause cache-only to be used.
// Default is cache-only.
cache.register(config);
writeRetryCount
const config = new KeyConfig("examples/{exampleName}");
config.writeCallback = async (context) => {
  return await someLongRunningFetch(context.params.exampleName);
};
config.writeStrategy = WriteStrategies.writeBehind; 
// retries the callback up to 4 further times if the initial attempt fails.
// only valid when writeStrategy is write-behind.
// default is 1 retry.
config.writeRetryCount = 4; 
cache.register(config);
writeRetryInterval
const config = new KeyConfig("examples/{exampleName}");
config.writeCallback = async (context) => {
  return await someLongRunningFetch(context.params.exampleName);
};
config.writeRetryInterval = 3000; // waits 3000ms before retrying the write callback.
cache.register(config);
writeRetryBackoff
const config = new KeyConfig("examples/{exampleName}");
config.writeCallback = async (context) => {
  return await someLongRunningFetch(context.params.exampleName);
};
config.writeRetryBackoff = false; // disable exponential backoff for retries.
cache.register(config);
writeRetryIntervalCap
const config = new KeyConfig("examples/{exampleName}");
config.writeCallback = async (context) => {
  return await someLongRunningFetch(context.params.exampleName);
};
config.writeRetryInterval = 3000; // waits 3000ms before retrying the write callback.
config.writeRetryIntervalCap = 10 * 60 * 1000; // never wait more than 10 minutes between retries of the write callback.
cache.register(config);
evictCallback
const config = new KeyConfig("examples/{exampleName}");
config.evictCallback = async (context) => {
  // the callback to run when a delete is triggered.
  // evictions share the write strategy and any write retry configuration.
  // throw an error if the delete does not complete.
  // if no error is thrown, the delete will be considered successful.
  // this callback shouldn't return anything.
  return await someLongRunningDeleteTask(context.params.exampleName);
};
cache.register(config);
updateCallback
const config = new KeyConfig("examples/{exampleName}");
config.updateCallback = async (context) => {
  // the callback to run when an update is triggered. this can be useful for when
  // you need to work with something that isn't idempotent, so you need different
  // actions for creates and updates .
  // updates share the write strategy and any write retry configuration.
  // throw an error if the update does not complete.
  // if no error is thrown, the update will be considered successful.
  // this callback shouldn't return anything.
  return await someLongRunningUpdateTask(context.params.exampleName);
};
cache.register(config);

Examples

Example 1: Caching HTTP API calls

Use read-through strategy to cache API responses and reduce network requests:

import { ExtensorCache, InMemoryStore, KeyConfig, ReadStrategies } from "extensor-cache";

const cache = new ExtensorCache(new InMemoryStore());
const apiConfig = new KeyConfig("api/users/{userId}");

apiConfig.readStrategy = ReadStrategies.readThrough;
apiConfig.ttl = 300; // cache for 5 minutes
apiConfig.readCallback = async (context) => {
  const response = await fetch(`https://api.example.com/users/${context.params.userId}`);
  if (!response.ok) throw new Error("Failed to fetch user");
  return await response.json();
};

cache.register(apiConfig);

// First call hits the API, subsequent calls use cache
const user = await cache.get("api/users/123");
console.log(user); // { id: 123, name: "John Doe", ... }

Example 2: Caching database queries with write-through

Ensure cache and database stay in sync with write-through strategy:

import { ExtensorCache, InMemoryStore, KeyConfig, ReadStrategies, WriteStrategies } from "extensor-cache";

const cache = new ExtensorCache(new InMemoryStore());
const dbConfig = new KeyConfig("db/products/{productId}");

dbConfig.readStrategy = ReadStrategies.readThrough;
dbConfig.writeStrategy = WriteStrategies.writeThrough;
dbConfig.ttl = 600; // cache for 10 minutes

dbConfig.readCallback = async (context) => {
  return await db.query("SELECT * FROM products WHERE id = ?", [context.params.productId]);
};

dbConfig.writeCallback = async (context) => {
  await db.query("UPDATE products SET data = ? WHERE id = ?", 
    [JSON.stringify(context.value), context.params.productId]);
};

cache.register(dbConfig);

// Read from database (or cache if available)
const product = await cache.get("db/products/456");

// Update both cache and database atomically
await cache.set("db/products/456", { name: "Updated Product", price: 29.99 });

Example 3: Write-back for non-critical analytics

Use write-back strategy with retries for analytics or metrics where immediate consistency isn't critical:

import { ExtensorCache, InMemoryStore, KeyConfig, WriteStrategies } from "extensor-cache";

const cache = new ExtensorCache(new InMemoryStore());
const metricsConfig = new KeyConfig("metrics/{eventType}/{userId}");

metricsConfig.writeStrategy = WriteStrategies.writeBack;
metricsConfig.writeRetryCount = 5;
metricsConfig.writeRetryInterval = 1000; // 1 second base interval
metricsConfig.writeRetryBackoff = true; // exponential backoff on retries

metricsConfig.writeCallback = async (context) => {
  await fetch("https://analytics.example.com/events", {
    method: "POST",
    body: JSON.stringify({
      eventType: context.params.eventType,
      userId: context.params.userId,
      data: context.value
    })
  });
};

cache.register(metricsConfig);

// Returns immediately, persists to analytics service in background with retries
await cache.set("metrics/page_view/789", { 
  page: "/products", 
  timestamp: Date.now() 
});

Strategies

Read Strategies

Read-Through

ReadStrategies.readThrough Use this if your read target is slow-changing. It will read cache first and only call the callback on a cache miss.

Read-Behind

ReadStrategies.readAround Use this if your read target is fast-changing. It will call the callback first and only go to cache if the callback returns a rejected Promise.

Cache only

ReadStrategies.cacheOnly Use this if you just want an in-memory cache. It will only read from the cache and not attempt to call any callback.

Write Strategies

Write-Through

WriteStrategies.writeThrough Use this if you care about consistency. It will only update the cache if the callback is successful.

Write-Back

WriteStrategies.writeBack Use this if you don't care about consistency. It will update the cache, return, then call the callback in the background. Retrying the callback can be configured, but will fail quietly when the retry limit is reached.

Cache only

WriteStrategies.cacheOnly Use this if you just want an in-memory cache. It will only write to the cache and not attempt to call any callback.


Options


Roll your own store

Go crazy. Check src/inMemoryStoreAdapter.js to get an idea of the necessary interface, then pass that to the cache at instantation.


Contributing

Contributions and new ideas are welcome! If you find a bug or have a feature request, please open an issue at:

https://github.com/jbrixon/extensor-cache-js/issues

When submitting changes via pull request, please:

  • Fork the repository and create a topic branch.
  • Add or update tests to cover your change where appropriate.
  • Run the linter and formatter (npm run lint / npm run format) and keep the code style consistent.
  • Provide a short, clear description of the change and the motivation in your PR.

Contributions are highly appreciated — thanks for helping improve the project!


Testing

Run:

npm test