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

@josephuspaye/stash

v0.2.0

Published

A simple cache with configurable storage and support for stale-while-revalidate

Downloads

4

Readme

Stash

Tests

🗃 A simple cache with configurable storage and support for stale-while-revalidate.

This project is part of #CreateWeekly, my attempt to create something new publicly every week in 2020.

How it works

Stash allows you to wrap an expensive function whose result you want to cache using stash.cache(). The function wrapped is called a producer, and a key is provided to use for caching the produced value.

The first time stash.cache() is called, the producer is called and the value produced is stored in the cache using the provided key. On subsequent calls, the cache is checked to see if there's an existing value for the key that hasn't expired. If there is, that value is returned without calling the producer. If there isn't, the producer is called to produce a new value, which is then cached and returned.

When stale-while-revalidate is enabled, expired items that haven't exceeded the staleWhileRevalidate value are returned immediately, and the producer is called asynchronously to update the value in the cache, for return on subsequent calls. See Using stale-while-revalidate for how to use.

Out of the box, Stash provides an in-memory storage for cached items, and you can provide your own storage.

The cache is updated only on demand, and stale items are not removed automatically. You can call stash.clearStale() periodically at an interval you choose to remove stale items.

See Usage for examples, and API for details.

Installation

npm install @josephuspaye/stash --save

Usage

Basic usage

The following example shows how to cache remotely fetched data for up to 5 minutes.

import { Stash, InMemoryStorage } from '@josephuspaye/stash';

const stash = new Stash(new InMemoryStorage());

function timeout(interval) {
  return new Promise((resolve) => {
    setTimeout(resolve, interval * 1000);
  });
}

async function fetchData(url, maxAge) {
  return stash.cache(url, { maxAge }, async () => {
    const response = await fetch(url);
    return response.json();
  });
}

async function main() {
  const fiveMinutes = 5 * 60;
  const url = 'https://swapi.dev/api/people/4/?format=json';

  // On first call, a request will be made and results will be cached for 5 minutes
  const data = await fetchData(url, fiveMinutes);
  console.log({ data });

  // Subsequent calls in the next 5 minutes will be resolved from the cache, without making a request
  const sameData = await fetchData(url, fiveMinutes);
  console.log(data === sameData); // true

  // Wait 5 minutes for `maxAge` to expire
  await timeout(fiveMinutes);

  // With the cache expired, the next call will make a request and return fresh data (which will then be cached)
  const newData = await fetchData(url, fiveMinutes);
  console.log(data !== sameData); // true
}

main();

Using stale-while-revalidate

The following example shows how to cache remotely fetched data for up to 5 minutes, with a subsequent 5 minute window where stale data will be returned from the cache while the data is revalidated (i.e. updated) asynchronously in the background.

import { Stash, InMemoryStorage } from '@josephuspaye/stash';

const stash = new Stash(new InMemoryStorage());

function timeout(interval) {
  return new Promise((resolve) => {
    setTimeout(resolve, interval * 1000);
  });
}

async function fetchData(url, maxAge, staleWhileRevalidate) {
  return stash.cache(url, { maxAge, staleWhileRevalidate }, async () => {
    const response = await fetch(url);
    return response.json();
  });
}

async function main() {
  const fiveMinutes = 5 * 60;
  const url = 'https://swapi.dev/api/people/4/?format=json';

  // On first call, a request will be made and the results will be cached for 5 minutes
  const data = await fetchData(url, fiveMinutes, fiveMinutes);
  console.log({ data });

  // Subsequent calls in the next 5 minutes will be resolved from the cache, without making a request.
  // Data resolved during this time is considered "fresh".
  const cachedData = await fetchData(url, fiveMinutes, fiveMinutes);
  console.log(data === cachedData); // true

  // Wait 5 minutes for `maxAge` to expire
  await timeout(fiveMinutes);

  // `maxAge` has been exceeded, making the cached data "stale". Because `staleWhileRevalidate` is set,
  // the stale data will be resolved from the cache immediately on the next call, while a request is
  // made in the background to update the data in the cache.
  const staleData = await fetchData(url, fiveMinutes, fiveMinutes);
  console.log(data === staleData); // true

  // The next call will get fresh data that was fetched when the previous call revalidated,
  // without making another request
  const revalidatedData = await fetchData(url, fiveMinutes, fiveMinutes);
  console.log(data !== revalidatedData); // true

  // Wait 10 minutes for `maxAge` and `staleWhileRevalidate` to expire
  await timeout(fiveMinutes * 2);

  // After the `staleWhileRevalidate` window expires, the next call will make a request and cache the results
  // for 5 minutes, just like the first call in this method above
  const newData = await fetchData(url, fiveMinutes, fiveMinutes);
  console.log(data !== newData); // true
}

main();

Using custom storage

You can use a custom storage backend to store cached items by implementing the Storage interface.

The following example shows how to use localStorage in a browser to store cached items. Note that this is a simple, unoptimized example for illustration only.

import { Stash, InMemoryStorage } from '@josephuspaye/stash';

class LocalStorage {
  constructor() {
    this.storageKey = 'stash-cache';
  }

  async getCache() {
    return JSON.parse(localStorage.get(this.storageKey) || '{}');
  }

  async size() {
    return Object.keys(await this.getCache()).length;
  }

  async has(key) {
    return (await this.getCache())[key] !== undefined;
  }

  async get(key) {
    return (await this.getCache())[key];
  }

  async set(key, value) {
    const cache = await this.getCache();
    cache[key] = value;
    localStorage.set(this.storageKey, JSON.stringify(cache));
  }

  async delete(key) {
    const cache = await this.getCache();

    if (cache[key] !== undefined) {
      delete cache[key];
      localStorage.set(this.storageKey, JSON.stringify(cache));
      return true;
    }

    return false;
  }

  async clearMatching(matcher) {
    const cache = await this.getCache();

    for (const [key, value] of Object.entries(cache)) {
      if (matcher(key, value)) {
        delete cache[key];
      }
    }

    localStorage.set(this.storageKey, JSON.stringify(cache));
  }

  async clear() {
    localStorage.set(this.storageKey, '{}');
  }
}

// Create stash instance with the custom LocalStorage backend
const stash = new Stash(new LocalStorage());

// use `stash` as normal...

API

InMemoryStorage class

An in-memory storage backend for the cache. Cached items are stored in a JS Map, supporting keys and values of any type.

Stash class

The main Stash class.

class Stash<K, V> {
  /**
   * Create a new stash with the given storage and default options. The default
   * options will be used when `stash.cache()` is called without options.
   * By default, `maxAge` is 1 hour, and `staleWhileRevalidate` is 0.
   */
  constructor(
    storage: Storage<K, V>,
    defaultCacheOptions?: Required<CacheOptions>
  );

  /**
   * Get the number of items stored in the cache
   */
  size(): Promise<number>;

  /**
   * Run the given producer, store the value it produces in the cache, and return the value.
   *
   * - If no value for the given key is in the cache, the producer is called and the value
   *   it produces is stored in the cache
   *
   * - If a value for the given key is in the cache, one of the following happens:
   *   - if the value is fresh (e.g. it hasn't exceeded `maxAge`, it is returned and the
   *     producer is not called
   *   - if the value is stale and can be revalidated (i.e. it has exceeded `maxAge`
   *     and `staleWhileRevalidate` is set and has not been exceeded) then the stale
   *     value is returned, and the producer is called asynchronously to revalidate
   *     (i.e. update) the value
   */
  cache(key: K, producer: Producer<V>): Promise<V>;
  cache(key: K, options: CacheOptions, producer: Producer<V>): Promise<V>;

  /**
   * Clear all stale items in the cache
   */
  clearStale(): Promise<void>;

  /**
   * Clear all items in the cache
   */
  clear(): Promise<void>;
}

Types

The following additional types are used in the API:

/**
 * Interface for cache storage backends
 */
interface Storage<K, V> {
  /**
   * Get the number of items stored
   */
  size(): Promise<number>;

  /**
   * Check if an item is stored with the given key
   */
  has(key: K): Promise<boolean>;

  /**
   * Get the value of the item stored with the given key. Returns the value if found, undefined otherwise.
   */
  get(key: K): Promise<CachedValue<V> | undefined>;

  /**
   * Store the given value with the given key
   */
  set(key: K, cached: CachedValue<V>): Promise<void>;

  /**
   * Delete the item with the given key. Returns true if an item was found and deleted, false otherwise.
   */
  delete(key: K): Promise<boolean>;

  /**
   * Delete all items that match with the given matcher
   *
   * @param matcher A matcher that takes the key and value of an item and returns true if the item
   *                should be deleted, and false otherwise
   */
  clearMatching(
    matcher: (key: K, cached: CachedValue<V>) => boolean
  ): Promise<void>;

  /**
   * Delete all items in storage
   */
  clear(): Promise<void>;
}

/**
 * A cached value
 */
interface CachedValue<V> {
  /**
   * The value
   */
  value: V;

  /**
   * When the value was stored in the cache, in seconds since the UNIX Epoch
   */
  storedAt: number;

  /**
   * How long an item should be in the cache before it's considered stale, in seconds
   */
  maxAge: number;

  /**
   * For how long a stale value should be returned after it becomes stale, in seconds
   */
  staleWhileRevalidate: number;
}

/**
 * A function (possibly async) that produces a value to cache
 */
type Producer<V> =
  | ((options: { isRevalidating: boolean }) => V)
  | ((options: { isRevalidating: boolean }) => Promise<V>);

/**
 * Options for caching items
 */
type CacheOptions = {
  /**
   * How long an item should be in the cache before it's considered stale, in seconds
   */
  maxAge?: number;

  /**
   * For how long a stale value should be returned after it becomes stale, in seconds
   */
  staleWhileRevalidate?: number;
};

Licence

MIT