Writing Service Workers in ReScript
Tags: bucklescript, rescript, pwa, functional
Published: 2021-04-14 18:28:55

---
title: Writing Service Workers in ReScript
published: true
date: 2021-04-09 00:00:00 UTC
tags: ["bucklescript", "rescript", "pwa", "functional"]
canonical_url: https://webbureaucrat.gitlab.io/articles/writing-service-workers-in-rescript/
---

Does your SPA work fully offline? Would you like to define a caching strategy in an exhaustively type-safe way? If so, you might be interested in [this Service Worker binding](https://www.npmjs.com/package/rescript-service-worker) for ReScript ([formerly BuckleScript / ReasonML](https://rescript-lang.org/blog/bucklescript-is-rebranding)). This article documents the binding by example, including two different caching strategies and a service worker registration example.

### The need for Service Workers in ReScript

JavaScript Service Workers are a way of intercepting HTTP requests in a web app and caching the result. This allows web app developers to write applications that work fully offline or supplement poor network speeds. They're extremely valuable in applications that have a lot of large assets such as SPAs (single page applications).

However, with great power comes great responsibility. The code that manages all HTTP requests can't afford to have _any_ bugs, but JavaScript is _so_ dynamic and _so_ weakly typed that it's very easy to inadvertently ship typos.

Enter ReScript, which is statically typed and more functional than TypeScript. It's much more difficult to introduce bugs in such a robust type system, which is why I use ReScript for all my Service Workers.

### Setting up your new service worker project

For a couple of reasons, it's almost entirely necessary to use a script bundler instead of ES6 modules. (It can be done with ES6 modules if you're able to mess with the security headers and trim the export statement off the bottom of the compiled script, but that's pretty cursed.) If you, (like me) don't usually do this, I wrote a quick example of[setting up webpack in ReScript](https://webbureaucrat.gitlab.io/articles/setting-up-webpack-for-rescript/)in a previous article.

In addition to a script bundler, install the `bs-fetch` and [`rescript-service-worker`](https://www.npmjs.com/package/rescript-service-worker)npm packages.

### Defining the service worker cache strategies

For completeness, this tutorial will include both a cache-first and a network-first component. This should allow the reader to easily adapt the sample code to other strategies.

My strategy for this site will be as follows:

- If the asset is in the asset cache:
  - resolve with the cache version
  - update the cache version in background
- Else (the asset is not in the cache)
  - If the network can be reached
    - resolve with the network version
    - update the runtime cache in background with the network version
  - Else (the network cannot be reached)
    - resolve with the runtime cache version

Basically, I have some assets like fonts and stylesheets which I don't think will change very much very often, so I want to use a cache-first strategy for them while keeping them up to date in background. However, I have content pages that should be as recent as possible while being able to fail over to a cache.

### Bindings and configuration

Personally, it's my practice to keep it to two or three open statements per ReScript file. In this case, because Service Workers are so tied to JavaScript Promises, it makes sense to `open Js;`.

I'm also going to `open ServiceWorker` because there are so many different modules from that package I will need. I would also highly recommend `open`ing `ServiceWorkerGlobalScope`, which, as the name suggests, exposes bindings to native JavaScript functions and values that are exposed by default in native JavaScript in the Service Worker context.

Outside of the [`rescript-service-worker`](https://www.npmjs.com/package/rescript-service-worker) bindings and the default `Js` bindings, there's only one more I'm going to add, which is the binding to the [JavaScript `error` constructor](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/Error)

```ocaml
@bs.new external makeExn: string => exn = "Error";
```

This will be used in a `Promise` constructor (basically to make the types work out).

#### Configuration

The next few lines represent some basic configuration for the app. There are two caches, which we will name "static" and "runtime" and we will version them in order to more quickly pick up changes when we need to.

```ocaml
/* configuration */
let version = "0.0.10"; 

let assetCacheName = Js.String.concat(version, "webbureaucrat-static-");

let runtimeCacheName = Js.String.concat(version, "webbureaucrat-runtime-");
```

### Adding requests to the static asset cache

This is a tricky binding, and I want to call special attention to it. In service workers, a cache is basically a key/value store, where a key can be either a URL string or a JavaScript Request object. Therefore, to properly bind to [`Cache.addAll`](https://developer.mozilla.org/en-US/docs/Web/API/Cache/addAll)we would need an array that can hold string or a Request or any combination thereof.

I went to [StackOverflow with this problem](https://stackoverflow.com/questions/65055018/how-do-i-use-an-unwrapped-polymorphic-variant-union-type-in-a-type-parameter), and you can see the solution there.

```ocaml
let assets = [
  Cache.str("/css/default.css"),
  Cache.str("/css/fonts/DejaVuSansMono-webfont.woff"),
  Cache.str("/css/menu.css"),
  Cache.str("/css/prism-base16-monokai.dark.css"),
  Cache.str("/css/site.css"),
  Cache.str("/css/util.css"),
  Cache.str("/img/icons/192.png"),
  Cache.str("/manifest.json"),
  Cache.str("/portfolio/"),
  Cache.str("/resume/"),
  Cache.str("/about/")
];

let precache = (): Promise.t<unit> =>
  caches(self)
  -> CacheStorage.open_(assetCacheName)
  |> Promise.then_(cache => cache -> Cache.addAll(assets))
;

self -> set_oninstall(event => {
  Js.log("The service worker is being installed.");
  event -> Notifications.ExtendableEvent.waitUntil(precache());
});

```

What's going on here is a Cache.addAll takes an array of `Cache.req`, a type that can be instantiated either via `Cache.request` or `Cache.string`. Here, since I'm only using string-representations, it makes sense to use `Cache.str` for all.

### Including helper methods for interacting with the service worker caches

Service workers are a relatively low-level API, so it's helpful to break our service worker cache strategy into building blocks that can be reused, so if we change our cache strategy later on, we will have to change very little actual code.

The `fromCache` function abstracts over fetching a response from a given cache, failing if it can't find the resource in cache. `fromNetwork` just passes straight through to the `bs-fetch` binding (for now), and `addToCache`, well, adds a request to a given cache.

```ocaml
let fromCache = (req: Fetch.Request.t, cacheName: string):
  Promise.t<Fetch.Response.t> =>
  caches(self) -> CacheStorage.open_(cacheName)
|> Promise.then_(cache => cache -> Cache.Match.withoutOptions(req))
|> Promise.then_(matching => switch(Js.Nullable.toOption(matching)) {
    | Some(m) => Promise.resolve(m);
    | None => Promise.reject(makeExn("no-match"));
  })
;

let fromNetwork = (req:Fetch.Request.t):
  Promise.t<Fetch.Response.t> =>
  Fetch.fetchWithRequest(req)
  
let addToCache = (req: Fetch.Request.t, cacheName: string):
  Promise.t<unit> => {
  caches(self)
    -> CacheStorage.open_(cacheName)
  |> Promise.then_(cache => cache -> Cache.add(#Request(req)))
};

```

These are small functions, but they'll make our lives a little easier in the next step.

### Defining both cache strategies

Now we can more easily define our two strategies we described at the top. The first, `assetStrategy`, takes grabs the asset from the cache and then updates it if it's in the asset cache.

The second tries the network first and then fails over to the runtime cache if there's a problem with the network.

```ocaml
let assetStrategy = (req: Fetch.Request.t, cacheName: string):
  Promise.t<Fetch.Response.t> => {
  let result = req -> Fetch.Request.makeWithRequest
    -> fromCache(cacheName);

  /* update cache iff. item already exists in cache */
  let _ = result |> Promise.then_(_ => addToCache(req, cacheName));

  /* don't wait on the update to return */
  result
};

let runtimeStrategy = (req: Fetch.Request.t, cacheName: string):
  Promise.t<Fetch.Response.t> => {
  let result = req -> Fetch.Request.makeWithRequest -> fromNetwork
  |> Promise.catch(_ => req -> Fetch.Request.makeWithRequest
                   -> fromCache(cacheName));

  let _ = addToCache(req, cacheName);

  result
};

```

#### A word on `Fetch.makeWithRequest`

This is the binding to the constructor that makes a copy of the given`Request`, and you'll notice I use it quite a bit. This is because`Request`s are stateful, which can very easily introduce some very weird and counterintuitive bugs. Therefore, I always make it a habit of copying a `Request` every time I reference it. Even if it's not always necessary, it means I can change my code without worrying whether I've introduced a state-management bug.

### `onfetch`

Because our code is well-organized, we can write our `onfetch` event handler in just three lines. Our response, per our requirements, tries the asset cache strategy and then fails over to the runtime cache strategy, using the versioned cache names we defined at the top of the file.

```ocaml
self -> set_onfetch(event => {
  let req = event -> FetchEvent.request;
  let resp: Promise.t<Fetch.Response.t> =
  req -> Fetch.Request.makeWithRequest
    -> assetStrategy(assetCacheName)
  |> Promise.catch(_ => req
                   -> Fetch.Request.makeWithRequest
                   -> runtimeStrategy(runtimeCacheName));
  event -> FetchEvent.respondWith(#Promise(resp));
});

```

Note: Service Workers themselves are scoped to their directory, so you'll probably want to locate your output at the root of your project. Unfortunately, there's [no easy way to do this with ES6 modules in ReScript](https://github.com/rescript-lang/rescript-compiler/issues/4857). Configuring a script bundler is out of scope for this article, but I have written a[tutorial for configuring webpack](https://webbureaucrat.gitlab.io/articles/setting-up-webpack-for-rescript/) for ReScript, and, as always, feel free to reach out or open an issue if you have trouble.

### Write the Service Worker loader

Lastly, if you're writing a Service Worker, you will need to run a script to register it, which is trivial.

Here's my solution, in a file called _SWLoader.res_:

```ocaml
open ServiceWorker;
open ServiceWorkerGlobalScope;

let path = "/SW.bs.js";

let _ = self -> navigator
  -> ServiceWorkerNavigator.serviceWorker
  -> ServiceWorkerContainer.Register.withoutOptions(path)
  ;

```

This assumes that my build outputs a file called _/SW.bs.js_ in the root of the directory. Configure your own path appropriately.

### Source / For Further Reading

This has been a tutorial on service workers in ReScript, based on this website's own service worker. If anything is unclear, feel free to view the [source in context](https://gitlab.com/webbureaucrat/webbureaucrat.gitlab.io/-/tree/master/scripts/wb-service-worker/src) on GitLab. You'll notice that my main assets like scripts and stylesheets are automatically available offline, and you'll find every article you visit on my site is available in the runtime cache. If something is still unclear, feel free to reach out or open an issue, and I'll be happy to take another run at it.