Utilities
RemixCache
A wrapper for the browser's Cache API. Use this to cache data in the browser with more control and less-hassle.
Introduction
Synopsis
Caching in the browser has been a thing that's existed for a long time. A simple key-value store (cache) for storing responses with requests as keys and it was packaged in
the Cache
API. We also have the CacheStorage
API that allows us to create and manage multiple caches. That was all that
caching responses in the browser involved, until now.
RemixCache
is a wrapper around the Cache
API that allows us to store and retrieve responses from the cache with more refined control.
It supercharges the Cache
API by adding some additional features to the normal caching process of storing and retrieving responses.
By default, items live in the cache forever with no limit, you are also allowed to store as much as you like which is fine,
till you want to finetune your cache. RemixCache
is a framework-agnostic wrapper (it works outside Remix too!) that utilises the browser cache with expiration, and size limits.
That's not all, it also allows integration with cachified
, a popular library for caching, shipping with all these right out the box!
Installation
To get started, run:
npm install `@remix-pwa/cache`
RemixCache
isn't directly available to be able to create and mutate, but no worries, cause @remix-pwa/cache
also ships with a RemixCacheStorage
which is available
in the global scope (worker & main thread). RemixCacheStorage
(alias Storage
) is a wrapper around the CacheStorage
API that allows us to create and
manage multiple caches of RemixCache
instances.
Basic Usage
Using @remix-pwa/cache
is no different to using the Cache
API. The only difference is to make sure you have opened the cache somewhere at the top of your service worker
before utilising it anywhere else to ensure it is always wrapped. It's no different from how it used to be in Remix PWA v2.x.x.
import { Storage } from "@remix-pwa/cache";
// Here we open a cache called 'my-cache'
// `cache` is an instance of `RemixCache`
const cache = await Storage.open("my-cache", {
// ...options for the cache
});
// somewhere in the abyss of my service worker...
cache.put("https://example.com", new Response("Hello World!"));
Some Advanced Stuffs
How does RemixCache
work? Can I use it with the normal browser CacheStorage
API?
Answering the last question first, yes you can! RemixCache
is a wrapper around the Cache
API, so it's just a normal cache.
First question is a bit more tricky. A little backstory might help with the understanding of how RemixCache
works.
A little backstory on how the cache package came about
I got the idea for RemixCache
from cachified
, after trying to
unsuccessfully bring more flexibity to the Cache
API in Remix PWA v2, I decided to re-attempt it in Remix PWA v3. Looking at a few caching solutions,
and testing them out with the new APIs you are looking at before you, I stumbled upon cachified
.
cachified
is a library for caching with your favourite caching solution, it allows you to cache via memory and Redis out-of-the-box. Plus, it's extensibility
means you can create adapters for it to cache with any other solution you want. I tested it out by creating an adapter for the Cache
API and it worked like a charm.
Next up was figuring how to replicate it. After a bit of inspection, I realised that cachified
actively modified the response (value) before storing
it. It was genius! You had your response which looked like this:
const response = new Response(value, {
headers,
status,
statusText
});
now wrapped into something like this:
const response = new Response(
JSON.stringify({
value,
metadata: {
/* Metadata. Where magic occured */
}
}),
{
headers: {
"Content-Type": "application/json",
headers
},
status,
statusText
}
);
I fell in love with the implementation. It was simple, fast enough and it worked. So I decided to go along with it.
I hope you read the backstory, if you didn't, you should. It's important to understanding how RemixCache
works. Now, let's get to the main question.
Looking at the backstory, you would see the new schematics that Response
were now wrapped with. It is all about the metadata. RemixCache
works by
simply comparing, deleting, adding, sorting based on the metadata. That way, you can introduce expiration, size limits and more to your cache.
Can you interface with it directly? Sure thing! RemixCache
is just a normal cache, so you can use it with the CacheStorage
API. To use it, you simply
need to remember that all caches of RemixCache
instance are prefixed by rp-
and the name of the cache. So if you have a cache called my-cache
, you can
access it via caches.open('rp-my-cache')
. You can also use Storage.open('my-cache')
to open it.
Looking at the model in the backstory above, I suppose I can leave you to your whims, fancies and imagination on what to do now that you have
a clearer picture of how RemixCache
works. I hope you enjoy using it as much as I enjoyed creating it for you ❤️.
API Reference
RemixCache
RemixCache
is the star of this package. The wrapper around the Cache
API that allows us to store and retrieve responses from the cache with more refined control.
Currently, instantiation of RemixCache
directly is impossible. And it was a purposeful decision, you can only nteract with via the Storage
API. This is to ensure that all caches are wrapped and you don't have to worry about it getting inconsistent later on.
First of all, RemixCache
implements the following interface:
interface CustomCache extends Omit<Cache, "addAll" | "matchAll"> {
put(request: RequestInfo | URL, response: Response, ttl?: number | undefined): Promise<void>;
}
It re-implements the put
method of the Cache
API to allow for expiration of items in the cache. It also adds a ttl
parameter to the method to allow you
specify the time to live of that particular item in the cache. If you don't specify a ttl
, the item will utilise the general ttl of the cache.
The other methods work the same way and accept the same arguments like the Cache
API. Check out the MDN docs for more info.
A look at the constructor of RemixCache
:
enum Strategy {
CacheFirst = 'cache-first',
NetworkFirst = 'network-first',
CacheOnly = 'cache-only',
NetworkOnly = 'network-only',
StaleWhileRevalidate = 'stale-while-revalidate',
}
constructor({
name: string;
strategy?: Strategy;
maxItems?: number;
ttl?: number;
}: RemixCacheOptions) {}
You should know!
Whilst runtime caching strategies in Remix PWA are handled via the @remix-pwa/strategies
package, the strategy
passed in here is useful (quite useful actually) when utilising it with tools like cachified
(which is supported out
of the box!).
The constructor accepts an object with the following properties:
name
: The name of the cache. This is used to identify the cache in theCacheStorage
API.strategy
: The caching strategy to use. This is useful when utilisingRemixCache
withcachified
. Defaults toStrategy.NetworkFirst
.maxItems
: The maximum number of items to store in the cache. Defaults to100
.ttl
: The default time to live of items in the cache. Defaults toInfinity
.
If you have any further questions or feel this section can be improved, please feel free to open an issue or PR.
RemixCacheStorage
RemixCacheStorage
is a static class that allows us to create and manage multiple caches of RemixCache
instances. A good question you might be having
now is why not a wrapper around CacheStorage
API. Good question, I have no idea myself. Wrapping it would mean making it a non-static class though,
and require you to instantiate it and pass it to the route workers via the getLoadContext
function (plus, it might be restricted to just the worker thread).
If you think that's a better idea, I would be happy to hear your thoughts on it (RFCs are welcome!).
As mentioned earlier, RemixCacheStorage
is alias to Storage
. You can use either of them, they are the same thing.
RemixCacheStorage
implements the following interface:
interface CustomCacheStorage {
createCache(opts: RemixCacheOptions): Promise<RemixCache>;
has(name: string): Promise<boolean>;
get(name: string): RemixCache | undefined;
open(name: string, opts?: Omit<RemixCacheOptions, 'name'>): RemixCache
delete(name: string);
clear();
}
And a few more that if you ask me, best to stay away from them.
The methods are pretty self-explanatory, but I would explain them anyway.
createCache
: Creates a new cache with the specified options. Returns a promise that resolves to the created cache.has
: Checks if a cache with the specified name exists. Returns a promise that resolves to a boolean.get
: Gets a cache with the specified name. Returns the cache if it exists, elseundefined
.open
: Opens a cache with the specified name. Returns the cache if it exists, else creates a new cache with the specified options and returns it.delete
: Deletes a cache with the specified name.clear
: Clears all caches.
Also a few things to note:
open
is preferred overcreateCache
as it is more performant. Use whenever you can.@remix-pwa/cache
also ships with two functions:initCache
andcreateCache
. Both are aliases to each other and are short hand forStorage.createCache
.
cachified
We haven't forgptten cachified
, in fact, we shipped an adapter just for it! @remix-pwa/cache
ships with an adapter
for cachified
that allows you to utilise the Cache
API (utilizing RemixCache
ofc). It also ships with a wrapper around cachified
itself
that provides you a short-hand to use it. It has the following interface:
export type CachifiedWrapperOptions = {
key: string;
cache: RemixCache;
getFreshValue: GetFreshValue<any>;
swr?: number;
ttl?: number;
staleRefreshTimeout?: number;
reporter?: CreateReporter<any>;
};
const cachifiedWrapper = (opts: CachifiedWrapperOptions) => {};
The cachifiedWrapper
function accepts an object with properties similar to the cachified
options. Check them
out here.
The key thing to note is that the strategy
parameter (of RemixCache
) is important here. For example, if you utilise a
NetworkOnly
strategy, you would not be able to cache anything because the ttl
would always be -1 (meaning, don't cache).
Also, if you utilise any strategy other than StaleWhileRevalidate
, swr
would not be applicable.
remixCacheAdapter
The remixCacheAdapter
is useful when you want more control over cachified
and want to use it with cachified
directly.
It is used to wrap RemixCache
into something cachified
can interface with and take in one parameter: cache
which is the RemixCache
instance.
import { remixCacheAdapter } from '@remix-pwa/cache'
const remixCache = await Storage.open('my-cache');
const cache = remixCacheAdapter(caremixCacheche);
await cachified({
cache,
key: request.url,
getFreshValue() {
return fetch(request);
},
});