Skip to main content
Home

A web Cache API standard implementation with custom persistence.

This package works with DenoIt is unknown whether this package works with Cloudflare Workers, Node.js, Bun, Browsers
It is unknown whether this package works with Cloudflare Workers
It is unknown whether this package works with Node.js
This package works with Deno
It is unknown whether this package works with Bun
It is unknown whether this package works with Browsers
JSR Score
70%
Published
3 days ago (0.2.2)

Web Cache API persistence

JSR JSR Score codecov

A web Cache API (almost) standard implementation that allows to use a custom storage/persistence layer.

Introduction

This package provides a CacheStorage (and Cache) implementation that mostly adheres to the standard Cache API defined by the Service Worker specification. We could say It is like a Ponyfill.

import { CacheStorage } from 'jsr:@esroyo/web-cache-api-persistence';

const caches = new CacheStorage();

// Usage is similar to the native `caches` property of the Window interface
const cache = await caches.open("my-cache");

Deno.serve(async (req) => {
  const cached = await cache.match(req);
  // ...

The main goal of the library is to allow to use your own persistence layer, while the application code continues depending on the standard Cache interfaces, and hopefully remains unaware of the real implementation used.

We can use our own persistence layer by implementing the CachePersistenceLike interface:

import { CacheStorage, type CachePersistenceLike } from 'jsr:@esroyo/web-cache-api-persistence';

class MyCachePersistence implements CachePersistenceLike {
  // ...
}

const caches = new CacheStorage(MyCachePersistence);

// Usage is similar to the native `caches` property of the Window interface
const cache = await caches.open("my-cache");

The persistence interface

The CachePersistenceLike interface specifies the core primitives for the storage. It resembles parts of the Cache and CacheStorage interfaces, but note It mixes concerns of both and has important differences:

/**
 * Provides a persistence mechanism to be used by the Cache object.
 */
export interface CachePersistenceLike {
    /**
     * The keys() method of the CachePersistence interface fulfills a similar role
     * to the keys() method of the CacheStorage object. The persistence layer has
     * to return the cache names for which it currently stores Request/Response pairs.
     *
     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/CacheStorage/keys)
     */
    keys(): Promise<string[]>;

    /**
     * The put() method of the CachePersistence interface is used by the
     * Cache object to store Request/Response pairs.
     *
     * The method responsability is limited to store the pair for latter usage.
     * Therefore, It should not perform checks on the given Request/Response objects.
     *
     * The specific implementations should decide the best way to perform
     * the storage operation, taking into account that for a given request
     * more than one response can exist.
     */
    put(
        cacheName: string,
        request: Request,
        response: Response,
    ): Promise<boolean>;

    /**
     * The delete() method of the CachePersistence interface is used by the
     * Cache object to delete an existing Request/Response pair, or all the
     * pairs associated the the same Request key.
     */
    delete(
        cacheName: string,
        request: Request,
        response?: Response,
    ): Promise<boolean>;

    /**
     * The get() method of the CachePersistence interface finds the entry whose key
     * is the request, and returns an async iterator that yields all the
     * Request/Response pairs associated to the key, one at a time.
     */
    get(
        cacheName: string,
        request: Request,
    ): AsyncGenerator<readonly [Request, Response], void, unknown>;

    /**
     * The [[Symbol.asyncIterator]] method of the CachePersistence interface returns
     * an async iterator that yields all the existing Request/Response pairs.
     * The pairs are returned in the order that they were inserted, that is older
     * pairs are yielded first.
     */
    [Symbol.asyncIterator](cacheName: string): AsyncGenerator<
        readonly [Request, Response],
        void,
        unknown
    >;

    /**
     * The [[Symbol.asyncDispose]] optional method of the CachePersistence interface
     * may be used to dispose internal resources used by the specific implementations.
     *
     * It will be called automatically if you open a Cache object with the `using` keyword.
     */
    [Symbol.asyncDispose]?(): Promise<void>;
}

Headers normalization

It is possible to provide a function to normalize headers by implementing the interface CacheHeaderNormalizer. Headers normalization is key to overcome Vary response headers that target request headers with great variation in the values (like User-Agent). Even when the you may think that the request headers may not vary so much, It is convenient to implement headers normalization to minimize the amount of Responses stored. Checkout this Fastly post for an extended explanation.

Imagine we have a Response that has the header Vary: User-Agent. Without headers normalization, for the same Request key we could potentially store hunders of different responses. To avoid this potential pitfall, we can normalize the header to just two values, either mobile or desktop:

import { CacheStorage, type CachePersistenceLike } from 'jsr:@esroyo/web-cache-api-persistence';

class MyCachePersistence implements CachePersistenceLike {
  // ...
}

const headersNormalizer = (headerName: string, headerValue: string | null): string | null => {
  if (headerName === 'user-agent') {
    if (headerValue.match(/Mobile|Android|iPhone|iPad/)) {
      return "mobile";
    } else {
      return "desktop";
    }
  }
  return headerValue;
};

const caches = new CacheStorage(
  MyCachePersistence,
  headersNormalizer, // pass the normalization function as second param
);
const cache = await caches.open("my-cache");

Deno.serve(async (req) => {
  // given a Request with "User-Agent: Mozilla/5.0 (Linux; Android 15) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.135 Mobile Safari/537.36"
  // or a Request with "User-Agent: Mozilla/5.0 (iPhone; CPU iPhone OS 17_7_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4.1 Mobile/15E148 Safari/604.1"
  // both would match...
  const cached = await cache.match(req);
  // ...

Additional modules

This package includes some CachePersistence implementations:

  • Memory (default): It stores the Request/Response pairs in a plain object, therefore It doesn't really provide persistence beyond the current process duration. It can be used for testing the library without further complications.
  • Deno KV: Implemente using kv-toolbox to provide arbitrarily large Response sizes.
  • Redis: Implemented with the Deno native client.

Key differences with the specification

Cache lifetimes

The spec states that "The Cache objects do not expire unless authors delete the entries."

In contrast, the provided CachePersistence implementations do honor the Response expiration headers, much like proposed by Deno. The Responses expire according to the Cache-Control or Expires headers when using some of the provided persistence implementations. However note this logic resides in the specific CachePersistence implementations, not in Cache/CacheStorage themselves, thus you can implement your own persistence and get rid of that behaviour.

Exceptions handling

The spec states that a if an Exception was thrown during a Batched Cache Operation, then all items from the relevant request response list should be reverted to the orignal cached values. This is not implemented.

New Ticket: Report package

Please provide a reason for reporting this package. We will review your report and take appropriate action.

Please review the JSR usage policy before submitting a report.

Add Package

deno add jsr:@esroyo/web-cache-api-persistence

Import symbol

import * as web_cache_api_persistence from "@esroyo/web-cache-api-persistence";
or

Import directly with a jsr specifier

import * as web_cache_api_persistence from "jsr:@esroyo/web-cache-api-persistence";

Add Package

pnpm i jsr:@esroyo/web-cache-api-persistence
or (using pnpm 10.8 or older)
pnpm dlx jsr add @esroyo/web-cache-api-persistence

Import symbol

import * as web_cache_api_persistence from "@esroyo/web-cache-api-persistence";

Add Package

yarn add jsr:@esroyo/web-cache-api-persistence
or (using Yarn 4.8 or older)
yarn dlx jsr add @esroyo/web-cache-api-persistence

Import symbol

import * as web_cache_api_persistence from "@esroyo/web-cache-api-persistence";

Add Package

vlt install jsr:@esroyo/web-cache-api-persistence

Import symbol

import * as web_cache_api_persistence from "@esroyo/web-cache-api-persistence";

Add Package

npx jsr add @esroyo/web-cache-api-persistence

Import symbol

import * as web_cache_api_persistence from "@esroyo/web-cache-api-persistence";

Add Package

bunx jsr add @esroyo/web-cache-api-persistence

Import symbol

import * as web_cache_api_persistence from "@esroyo/web-cache-api-persistence";