@tkesgar/reine

Stale-while-revalidate caching utility library

Usage no npm install needed!

<script type="module">
  import tkesgarReine from 'https://cdn.skypack.dev/@tkesgar/reine';
</script>

README

@tkesgar/reine

Build Status codecov npm bundle size npm TypeScript tested with jest

Ketika ngirim foto cookies buat Moona-senpai terus dikira foto telur dadar... 😭

reine provides createSWR, a simple in-memory caching helper based on the stale-while-revalidatestrategy:

// Assume that renderToString takes 1000 ms
const renderPage = createSWR(() => renderer.renderToString(app, ctx));

app.use(async () => {
  const html = await renderPage();
  return html;
});

It is ~500 bytes gzipped, has no dependencies, and can be used in Node.js or browser environment.

Pavolia Reine by 飯田ぽち

Pavolia Reine artwork by 飯田ぽち

Installation

$ npm install @tkesgar/reine

Usage

The module exports a function createSWR.

import createSWR from "@tkesgar/reine";

createSWR(asyncFn, opts = {})

Wraps asyncFn using stale-while-revalidate strategy into a new asynchronous function.

Available options:

  • maxAge (default: 1000): the minimum age in miliseconds for the value to be considered stale.
  • staleAge (default: 2000): the minimum age in miliseconds for the value to be considered old.
  • revalidateErrorHandler: an error handler that will be called with the error if asyncFn throws an error when trying to revalidate (i.e. value is style).
  • initialValue: if a value is provided, it will be used as initial value. The SWR instance will always start at fresh state.
const wrappedFetchData = createSWR(fetchData, {
  maxAge: 30000,
  staleAge: 60000,
  revalidateErrorHandler(err) {
    log.error({ err }, "Failed to fetch data from network; using stale data");
  },
  initialValue: null,
});

wrappedFn.age: number

Returns the age of current value.

If the value is not available yet, the value will be Infinity.

wrappedFn.status: "fresh" | "stale" | "old"

Returns the current state of value based on its age.

wrappedFn.reset([value])

If no value is provided, sets the status to be "old", causing the next call to execute the function.

If a value is provided, sets the current value to the provided value and refreshes the cache.

wrappedFn(): Promise

Retrieves the value for the function. Depending on the status:

  • If status is fresh, the currently available data will be returned. The function will not be executed.
  • If status is stale, the currently available data will be returned. However, the function will be executed and the value will be "refreshed".
    • If the function throws an error and revalidateErrorHandler is available, it will be called with the error value as argument.
  • If status is old, the function will be executed and the value is returned.
    • If the function throws an error, the function will throws the error.

Recipes

Simple usage

const fetchWinner = createSWR(() => fetchData("/api/user/winner/info"));

const winner = await fetchWinner();
console.log(await winner.username);

Log revalidation errors

If there is no error handler, the error will be silently ignored; the SWR instance will return the stale value instead.

It is recommended to provide an error handler to some logging mechanism.

const fetchWinner = createSWR(() => fetchData("/api/user/winner/info"), {
  revalidationErrorHandler(err) {
    log.warn({ err }, "Failed to fetch winner info; using stale render result");
  },
});

const winner = await fetchWinner();
console.log(await winner.username);

Providing initial value

If an initial value is provided, the state will start as "fresh". This avoids the first call to be delayed.

const fetchWinner = createSWR(() => fetchData("/api/user/winner/info"), {
  initialValue: { id: 323, username: "pavolia_reine" },
});

const winner = await fetchWinner();
console.log(await winner.username);

Preload SWR instance

To avoid delays or errors for the first call, simply call the function first.

const fetchWinner = createSWR(() => fetchData("/api/user/winner/info"));
await fetchWinner();

const winner = await fetchWinner();
console.log(await winner.username);

It is possible to make preload non-blocking; however, be aware that the future call will throw error if the function throws error again.

const fetchWinner = createSWR(() => fetchData("/api/user/winner/info"));
fetchWinner().catch((err) => {
  log.warn({ err }, "Failed to fetch winner info");
});

const winner = await fetchWinner();
console.log(await winner.username);

Always revalidate

Setting maxAge to 0 and staleAge to Infinity will cause the function to always revalidate the value on every calls, but returns the stale value instantly. This behaviour might be desirable if stale values are acceptable and revalidation is "cheap".

const fetchWinner = createSWR(() => fetchData("/api/user/winner/info"), {
  maxAge: 0,
  staleAge: Infinity,
});
await fetchWinner();

const winner = await fetchWinner();
console.log(await winner.username);

Cache invalidation

The cache can be invalidated for certain use cases, such as receiving an update event for the cached resource.

const fetchWinner = createSWR(() => fetchData("/api/user/winner/info"), {
  revalidateErrorHandler(err) {
    log.warn({ err }, "Failed to fetch winner data");
  },
});

externalService.on("match-finished", () => {
  fetchWinner.reset();
});

To avoid delays, simply call the function after invalidating the cache.

externalService.on("match-finished", () => {
  fetchWinner.reset();
  fetchWinner().catch((err) => {
    log.warn({ err }, "Failed to fetch winner data");
  });
});

Alternatively, it is also possible to set the current value.

externalService.on("match-finished", (result) => {
  fetchWinner.reset(result.winner);
});

Questions

Does createSWR supports custom cache key/function arguments?

No, createSWR call returns a single wrapped function that will be executed without arguments.

Does createSWR supports custom cache storage (e.g. Redis)?

No, only in-memory.

Contribute

Feel free to send issues or create pull requests.

License

Licensed under MIT License.