Introduction

Like many, I am excited by the resurgence of interest in offline web applications.

I’m also amused by it. Offline apps were all the rage a dozen years ago or so, but then everyone got excited about native. Facebook famously made the mistake of doubling down on mobile web and abruptly switching to “mobile first” shortly before their IPO.

I remember reading a quote years ago-and I wish I could remember the exact wording-about how the tech industry is stuck in an endless loop. People propose the same things over and over and think its new. I’m rather startled to discover that I’m old enough to have seen this observation proven multiple times.

To quote one of my favourite authors and writing instructors, Brandon Sanderson, “Everyone wants to be new and edgy, but they all end up being new and edgy in the same way”. He was talking about writing in the second person, but it applies to the entire silicon valley startup ecosystem, too.

Forgive an old man for digressing… now where was I? ;-)

Oh yes! I’ve got this long-running series of articles about the Rescript programming language. I’ve recently been looking for a project that would allow me to study binding Rrightescript to third-party libraries and I was hoping to experiment with promises as well. Building an offline-enabled Rescript app allowes me to do both.

Patreon

This series takes a lot of time to write and maintain. I’ve been working solid 8 hour days on these articles for a few weeks to get the quantity and quality of content you are reading. I won’t be able to keep that up once my next job begins, and without some level of motivation, I’ll probably drop the series altogether.

On the other hand, if you’re interested in providing some of that motivation, I’ve set up a Patreon account. I thank you for your support. I am not expecting this to pay for my time investment, but it helps to know if people appreciate it.

As a bonus for patrons, articles will be published there at least two weeks before they make it to my public blog.

Other ways to show support include sharing the articles on social media, commenting, or a quick thank you on the Rescript forum.

With over a dozen articles and counting, I’ve created a table of contents listing all the articles in this series in reading order.

Overview of the project

I’m going to keep running with the recipes app example I’ve been using, but will be starting from scratch instead of extending the existing project. This will allow us to take advantage of a create-react-app template specifically designed for progressive web applications (it’s not terribly different from the default one).

We’ll be using the RxDB database to do the offline storage. It has some really nice reactive elements and can be connected to Graphql in the future to sync to a cloud db.

As a warning, this is a big project and is going to take several articles.

  • You might find the official create-react-app docs valuable reading
  • Create-react-app is iscluding Workbox for the service worker stuff
  • RxDB will provide our offline data storage
  • Hasura has a great course on offline-enabled apps (Hasura is an outstanding project with the best documentation I have ever read).

Let’s begin

Start by creating a react app with the PWA (progressive web app) features enabled:

npx create-react-app rescript-offline --template cra-template-pwa

Open index.js and remove two characters: change serviceWorkerRegistration.unregister() to serviceWorkerRegistration.register().

This, by default, caches all the static assets for your site. It works offline. So we’re done!

The commits for this project can be found in the db-connections branch of the rescript-offline repo if you want to follow along. It can be helpful to see how the JS output (also checked in) of the rescript compiler changes as you modify the .res files.

Does it work offline?

Run npm run start and navigate to localhost:3000 in Chrome. Other browsers probably behave similarly, but I haven’t checked.

Show the dev tools and navigate to the Application tab. Tick the Offline checkbox, and, finally, refresh the page.

If you did everything right you should see “No Internet”.

Wait, what?

Actually, service workers are not enabled when running the dev server. You really don’t want stuff to be cached when you’re expecting them to update during development!

Let’s try this again. First, untick the Offline box so it doesn’t mess you up later.

Then run npm run build. This will spit out a build directory with all your assets (this is what you’d deploy to production).

cd build and run npx http-server to start an http server running the “production” build. Now head to localhost:8080 since http-server defaults to a different port.

Open the dev tools again and tick that Offline checkbox in the Applications tab. If you’re wondering why I told you to untick it and then tick it again, remember that you’re effectively accessing a different site (on port 8080). It’s a completely different data store.

Refresh the page. It should still render, though if you check the network tab, it won’t have made any requests. Alternatively, uncheck the Offline box and hit ctrl-C in the terminal to exit the http-server process.

Reload the page again. It should render as if the server were still running.

Now let’s get a feel for something that can go wrong when working with service workers. Edit App.js and change the text contents.

Run npm run build and start the npx http-server process again.

Refresh the page in the browser and note that it does not contain your new changes. By default, the service worker will not load the new content until you close all tabs connected to it.

Try that: close the tab, then open a new one and navigate to localhost:8080.

Now it should show the edited version of the file.

To get around this when you are testing stuff locally, you can use the Update on reload box in the dev tools. It’s right beside Offline. However, you do need some kind of strategy to instruct the user to reload service workers when releasing new versions of an app.

Port the app to rescript

Now that we have an offline-capable javascript app, let’s port it to Rescript! Luckily, we already know how to do this. I knew I wrote that article for a reason!

If you don’t want to do all that, you can just checkout the basic-cra-sw branch of my rescript-offline repo.

If you do want to do it yourself, you can pretty much follow the setting up a rescript app word for word.

I didn’t bother porting the service worker code itself to rescript.

There is one minor change to the Index.res file. We need to register the service worker. by importing the register function from `serviceWorkerRegistration and calling it.

So put

@module("./serviceWorkerRegistration") external register: unit => unit = "register"

at the top of Index.res.

And put register() at the end.

A caution on adding more static files

If you add a new image image to the static/ folder, you can’t just reference it as <img src="/rescript-brandmark.png" />. The problem is that create-react-app won’t know to update the manifest to precache that image, so it won’t be loaded into the browser cache until you visit a page that requires it while connected to the network.

Instead, do the same thing we did in the earlier article with the logo:

@module("./rescript-brandmark.png") external rescriptLogo: string = "default"

<img src={rescriptLogo} />

Always make sure any files you want to include in the manifest come through create-react-app loaders.

Introducing RxDB

We’ll be using RxDB for our offline database. It is a couchdb-like nosql store that works in the browser (on top of IndexedDB). It is “Reactive” meaning that changes to the database can fire events on subscriptions. This effectively means your React state can BE your db state.

Most notably for this tutorial, RxDB does not have Rescript bindings that I could find. So we get to implement our own! I’m not going to implement every method on every object in the project; just the ones we need as they come up. This is a recommended approach for Rescript-Javascript interop. In fact, the Rescript team doesn’t generally recommend making third-party Rescript libraries to wrap JS!

As all great projects must, start with installing dependencies:

npm install --save rxdb rxjs pouchdb-adapter-idb.

Create a new Db.res file and add some imports from the Javascript RxDB package. We’ve used this syntax in a couple earlier articles, but this is our first deep look at binding to JS:

@module("pouchdb-adapter-idb") external pouchDbAdapter: 'whatever = "default"
@module("rxdb") external addRxPlugin: 'whatever = "addRxPlugin"
@module("rxdb") external createRxDatabase: 'whatever = "createRxDatabase"

Ignoring all the gobbledygook, this is just creating three variables, as though we had called let pouchDbAdapter = .... Of course, the gobbledygook has meaning:

  • The external tells Rescript that it is defining a binding to a value defined in an external javascript module or file
  • @module(...) tells us which javascript (npm) package the external value is inside. It could also refer to a specific Javascript file in your repo, as we did above with the serviceWorker. Basically, this is whatever you would put at the end of an import statement.
  • 'whatever in all three cases is the type of the object that is being bound to. For now, we are lazily using 'whatever. The ' indicates it’s a generic type; it is a standin for an “any” type, and we can treat it as an opaque Javascript object. This obviously isn’t typesafe in Rescript, but it lets us get up and running quickly.
  • = "default" tells Rescript to bind to the default export in that module. You might also put a specific value.
  • = "createRxDtabase" and = addRxPlugin tell rescript to bind to those specifically export names in whichever module was passed to @module (in this case, rxdb). For comparison, in Javascript you’d import these as import { addRxPlugin, createRxDatabase } from 'rxdb'. Basically, whatever goes after the = sign in an @module declaration goes inside curly braces in an import statement.

Next, RxDB requires us to install an adapter for the database backend. In our case, we’ll be using a pouchDbAdapter that stores everything in IndexedDB in the browser. RxDB actually works server-side with a different set of adapters, but we won’t be using that functionality today. Install the pouchDbAdapter with:

addRxPlugin(pouchDbAdapter)

This just calls the addRxPlugin function that we imported as an external javascript property from rxdb. We are passing in the pouchDbAdapter that we imported as an external for the default import from pouchdb-adapter-idb.

Looking at this, we can make the addRxPlugin type a little more obvious. We don’t need to care what type pouchDbAdapter is, so let’s give it a generic type 'pouch But we do know addRxPlugin is a function that accepts that generic type and returns nothing. So we can change the typings on those two declarations to:

@module("pouchdb-adapter-idb") external pouchDbAdapter: 'pouch = "default"
@module("rxdb") external addRxPlugin: 'pouch => unit = "addRxPlugin"

Creating a database

We’ll improve the types the createRxDatabase in a bit, but first let’s see if we can call it as is. Add a make function to the Db.res module:

let make = () => {
  createRxDatabase({"name": "recipes", "adapter": "idb"})
}

Next open App.res and add a call to this new function from inside the Apps make function:

let db = Db.make()
Js.log(db)

Run the app to test it out. I recently discovered that my vim rescript plugin is automatically building files for me most of the time, so I only have to run yarn start and not yarn start:res.

Open the browser console and see if it logged anything. If all goes well, it should log a Promise.

Time to learn about async and await in Rescript. Spoiler alert: There is no async and await in rescript. My impression is that the Rescript devs are planning for it, but…. no promises.

Rescript does have bindings to JS promises, but they are clunky because they are optimized for pipe-last (|>) syntax instead of the pipe-first (->) operator that Rescript encourages.

So, instead, we’ll use the rescript-promise package, which is maintained by a core Rescript developer. I believe it is going to be added to the Rescript Js API in the future, probably as a Js.Promise2 package.

First npm install --save @ryyppy/rescript-promise. Don’t forget to open bsconfig.json and add this to the bs-dependencies: "@ryyppy/rescript-promise".

Now we can pipe the output of createRxDatabase (which we now know, from our logging, is a promise) into the Promise.then function. For now, all I want to do is log the value the promise returns to see what it is:

let make = () => {
  createRxDatabase({"name": "recipes", "adapter": "idb"})->Promise.then(db => {
    Js.log2(`Loaded database`, db)
    db
  })
}

Note the use of log2 which means “log two arbitrary arguments”. We were previously using log, which means “log exactly one argument”.

Now I see two lines of output in my console:

> Promise {<pending>}
> Db.bs.js:14 RxDatabaseBase {internalStore: PouchDB, idleQueue: IdleQueue, token: "bcnqwb9xhl", _subs: Array(1), destroyed: false}

The first line is from the original log we put in App.res. If you aren’t familiar with promises, this shows that the Promise is returned immediately, before whatever is inside the then function is ready to execute. The second line is the new log line, which shows that we’ve successfully constructed a RxDatabase.

Improve typing on the createRxDatabase parameter

createRxDatabase and the RxDatabase it returns are important user-facing functions. We should provide safe Rescript types for them instead of the generic type we started with.

The first thing we can do is note that createRxDatabase is a function. So we can change the type to 'whatever => 'whateverelse.

We also know the return value is a Promise, which improves our type to 'whatever => Promise.t<'rxdbThingy>.

Promise is the module we imported before, while t is the type inside that module that represents JS Promises.

But we still have two generics. We can fix one of them by adding a new type for the database parameters. The parameters are summarized here. If you are doing your own bindings to libraries, I’ve found it’s also helpful to look at the typescript bindings, such as here.

I don’t feel like modelling all of the paramters fields right now. We are not building a stand-alone library for public consumption. So I’ll just model the name and adapter, which I’m already passing in. The options I do choose to model can be encapsulated in a normal Rescript record type (This needs to go before the createRxDatabase external declaration):

type createRxDatabaseOptions = {
  name: string,
  adapter: string,
}

Now we can change the createRxDatabase external type to createRxDatabaseOptions => Promise.t<'rxdbThingy>.

Rescript will now raise an error because we were previously passing an object into createRxDatabase, but we have now told the compiler that the function accepts a record. These are distinct concepts in Rescript, though I won’t go into the details right now. Syntactically, they are very similar except:

  • Objects have keys wrapped in quotes
  • Records have bare keys

Just remove the quotes from the words "name" and "adapter" in the call to createRxDatabase as follows:

createRxDatabase({name: "recipes", adapter: "idb"})->Promise.then(db => {
  Js.log2(`Loaded database`, db)
  db
})

Building the RxDatabase type

Now we are ready to tackle the final generic in createRxDatabase: the return type inside the promise.

That type will be modelled as the t type for the Db.res module. Interestingly, we don’t need to specially define anything about t except that it exists. Just add type t at the top of the file. It doesn’t need any fields, but it needs to go before the createRxDatabase external so the external can reference it.

Then make one final change to the createRxDatabase external definition: the return type changes from Promise.t<'rxdbthingy> to Promise.t<t>.

This is sufficient for rescript to infer that t is the type of the js object returned by createRxDatabase.

Now we can write functions that guarantee they accept that object without rescript actually needing to know anything else about it.

However, we now have a compiler error. The db return type from Promise.then is illegal. Promise.then is defined such that then must return a Promise.t. Luckily, it’s easy to return a Promise: Just call the Promise.resolve function, passing in the return value. There are two ways you can do this; take your pick:

  • Promise.resolve(db)
  • db->Promise.resolve

The behaviour in browser should be unchanged. It should still print two things to console:

  • The Promise returned by createRxDatabase
  • the Loading database <RxDatabaseBase>

Modelling a method on RxDatabase: destroy

Modelling methods isn’t too hard, but the syntax is intimidating at first. The most difficult part is mapping the default object oriented “objects have methods” mental model to the functional “functions accept objects” model.

RxDatabase has several useful methods that we may want to model. We’ll start with the destroy method because it’s pretty simple and we know we’ll need it for full lifecycle management.

According to the docs for destroy:

  • it is a method
  • it takes zero arguments
  • it returns a Promise of nothing

However, our function will need to take a t as one argument, in lieu of an object having a method.

In rescript, we use @send to bind a function to a rescript method. The syntax is is essentially the same as @module external. Both have the format:

<decorator> external <name>: <type> = <js name>

For methods, we just use a different decorator. So it looks like this:

@send external destroy: t => Promise.t<unit> = "destroy"

Pretty straightforward, really. When I first encountered it in the docs, I was feeling a bit intimidated by binding to js, but once you break it down as above, it’s not too bad.

Now, instead of calling ‘db.destroy()’ as we did in Javascript, we can call either destroy(db) or db->destroy.

Using an effect

Let’s actually use this destroy method by adding a cleanup effect to App.

First, change the app to store the db inside a useState and set it from inside a useEffect. This allows us to set the db only after the promise resolves:

let (db, setDb) = React.useState(() => None)
React.useEffect1(() => {
  let dbPromise = Db.make()->Promise.map(db => {
    setDb(_ => Some(db))
    db
  })
  None
}, [setDb])

Js.log2("db is", db)

There’s a lot to pay attention to in a few lines here! As we’ve seen in earlier react articles, useState must be called with an initialization function. In our case, we initialize it to the None option.

Another Rescript oddity is that useEffect1 needs to inform rescript how long the dependency array is. That’s what the 1 in useEffect1 is for. If you had two deps, it would be useEffect2. It’s similar to log2.

Also somewhat unexpectedly, useEffectX needs to return an option. If you are use to Javascript React, you probably don’t return anything from useEffect very often. This means you are implicitly returning undefined. In Rescript you have to do that explicitly.

When the function returns a Some, the value of the option is the cleanup function. We haven’t reached that point yet, so we’ll just return None.

Also recall that our Db.make function returns a Promise.t. We pass the promise into the Promise.map function. Promise.map is identical to Promise.then except the callback function doesn’t have to return a Promise.t. map will automatically wrap the returned value in a new Promise.t. It’s the same idea as Option.map.

When the Db.make promise resolves, we pass the result to setState. Finally, note how we assign the promise that creates the database to the dbPromise variable. We’ll use this in our cleanup function later to access the database.

If you run the app now, the Js.log2 line should be called twice:

  • “db is None” on the first render
  • “db is <RxDatabaseBase>” after the effect completes

Returning a cleanup function from the effect

We can replace the None above with a Some(() => {...}). The callback inside the Some will call be able to call Db.destroy on the underlying db. we can get the db out of the dbPromise using Promise.then. Note that we don’t want to access the outer db from useState directly because the effect should not depend on that value. Otherwise the effect would be called every time the db changes, which would result in a dreaded render loop.

Here`s the new return value:

  Some(
    () => {
      let _ = dbPromise->Promise.then(db => db->Db.destroy)
    },
  )

We are piping the dbPromise into Promise.then. The promise has probably already resolved by the time the cleanup function is called, but it doesn’t really matter.

Whenever the promise does resolve, the db gets piped into the Db.destroy method we originally wrote.

Db.destroy returns a Promise.t, and we can just let Rescript return that directly becasue then expects to be returning a Promise. So we don’t need an explicit Promise.resolve this time.

We assign the resulting promise to the “unknown” variable, _. You might have expected to be able to just ignore the value, but rescript requires all function calls that return a value to assign that value. The error message for this is a little ambiguous, so make sure to look out for it.

This section has been a little complicated, so here’s the whole useEffect1 for comparison:

  let (db, setDb) = React.useState(() => None)

  React.useEffect1(() => {
    let dbPromise = Db.make()->Promise.map(db => {
      setDb(_ => Some(db))
      db
    })

    Some(
      () => {
        let _ = dbPromise->Promise.then(db => db->Db.destroy)
      },
    )
  }, [setDb])

Modelling the recipes type

Rxdb is a document-oriented database. We need to specify the structure of those documents using a schema. Before we do that, though, let’s define the rescript types we will use. These are the types we use in our code, as opposed to the ones stored in the database. We’ll map the db types to the Rescript types in a later step.

We can adapt the types we used in the Store for our introductory React app. However, I want to add some extra type documentation around the fields. And I want to add the id field that I overlooked in the original react article.

Create a new file named Model.res:

open Belt

type id = string
type title = string
type ingredients = string
type instructions = string
type tag = string

type recipe = {
  id: id,
  title: title,
  ingredients: ingredients,
  instructions: instructions,
  tags: array<tag>,
}

type taggedRecipes = {
  tag: tag,
  recipes: array<id>,
}

I created several new types that all map directly to primitives. They honestly don’t do much other than supply a bit of documentation. Hopefully they will help us avoid, for example, submitting instructions where ingredients were expected. RxDB expects primary keys to be strings, so I made id a string type. This is in contrast to what I did in the previous article on an express server. Don’t worry, though, I have an article queued up on how delightful it is to refactor in Rescript to fix that!

The recipe and taggedRecipes types are the meat of our stored state.

Modelling the schema

RxDB expects schemas to be modeled in the json-schema standard, although it only supports a subset of json-schema fields.

We did a lot of JSON in the express article, but let’s do a recap to explore our options. There are four ways to model JSON in Rescript:

  • Bind to the javascript JSON.parse function and blindly assign the output a type. This will throw an exception if it is unparsable JSON, and will simply behave incorrectly if it has the wrong type. So only use it on JSON that you trust to be correctly formatted.
  • Use the Js.Json module to parse individual properties one at a time. This requires verbose code and is somewhat frustrating to write, but it’s guaranteed to be type-safe. We covered a lot of that in the rescript-express article.
  • Use a third-party library such as bs-json to write JSON decoders and encoders. This is type-safe. I would ordinarily recommend it by default, but bs-json suffers from being designed to work with Reason’s “pipe-last” syntax, which feels unergonomic in Rescript. There is a new library named jzon you might want to look at.
  • Leave the json type in Javascript and just bind it to a generic in Rescript. This isn’t useful in all situations, but it will work perfectly for the purpose of our JSON schema.

Create a new file named src/Schema.json as follows:

{
  "recipes": {
    "title": "recipes schema",
    "version": 0,
    "description": "A single recipe",
    "type": "object",
    "properties": {
      "id": {
        "type": "string",
        "primary": true
      },
      "title": {
        "type": "string"
      },
      "ingredients": {
        "type": "string"
      },
      "instructions": {
        "type": "string"
      },
      "tags": {
        "type": "array",
        "uniqueItems": true,
        "items": {
          "type": "string"
        }
      }
    }
  },
  "tags": {
    "title": "tags schema",
    "version": 0,
    "description": "Mapping of tags to recipe IDs",
    "type": "object",
    "properties": {
      "tag": {
        "type": "string",
        "primary": true
      },
      "recipes": {
        "type": "array",
        "items": {
          "type": "string"
        }
      }
    }
  }
}

Hopefully this is easy to read, even if you aren’t familiar with jsonschema. We have defined two “collections” named recipes and tags. The former contains documents with the id, title, ingredients and instructions of the recipe, as well as an array of tag names. The tags collection is like an index; it contains documents with a tag name and array of recipe ids with that tag.

The version is probably one thing that needs explaining. RxDB will expect you to provide a migration step if this version number is not 0.

Now we need to load this schema in our Db.res file. This is easier than you might expect:

@module("./Schema.json") external schema: 'schema = "default"

Js.log(schema)

Because we used create-react-app, and the default webpack pipeline for CRA knows how to load json, we just have to bind a value to the file in question. Breaking it down in case you haven’t quite internalized the format of an external declaration yet:

  • @module("./Schema.json") tells Rescript which module to look for the value in
  • external tells Rescript it’s an external JS binding
  • schema is the name of the imported value in the Rescript namespace
  • 'schema is the type of the imported value. In this case, we use a generic type (denoted by ') to indicate we either don’t care about the type or will specialize it later.
  • default is the name of the value being imported from the Javascript namespace. In this case, it’s default, since webpack will spit the json object out as a top-level value.

The Js.log line allows us to double check that the value was imported correctly.

One thing I like about this is that we pragmatically decided to write JSON in JSON instead of rigorously typing it. There may be bugs in the way this JSON is written, and we won’t find those bugs until runtime. However, in practice, we would find them immediately because we can’t do anything without a functioning schema in the database. So there’s no point in wasting a lot of time trying to model the types of the full jsonschema spec.

That said, if you were trying to wrap the RxDB library as a public rescript module, instead of just wrapping the bits of it that we need for this project, you’d probably want to supply stronger type guarantees to your API consumers. Be pedantic only when it is pragmatic to do so.

Adding db collections

The RxDB documentation on addCollectios makes it sound simple, but the process of mapping it to Rescript is not. I spent a solid day sorting out what needed to be done to write this section!

We’ll need to do several things, and I’ll outline them before we start so you can see where I’m going:

  • Model the paramater that gets passed into addCollections. It can take several options, but for starters, we’ll just model the schema, which is required.
  • Model the addCollections method itself as a function call. It accepts a dictionary of AddCollectionsOptions and returns a Promise.t of the collections that have been added. I had to look at the source code to determine what the return value is.
  • Construct the options that get passed into addCollections for our specific usecase (ie: recipes and tags collections).
  • call addCollections with those options.
  • Handle the returned promise.

Modelling addCollectionsOptions

According to the RxDB docs and source code, addCollections accepts a dictionary object where the keys are names of collections, and the values are an object with several fields. Our goal is to model that object.

The most important of these fields is the schema. In fact, I’m not even going to bother modelling the other properties that can go on there yet. We can flesh it out, if necessary, once the end to end experience is in place.

Further, the schema is going to be different for each collection. Our recipes collection has a different schema from tags. This means that schema will need to be a generic type.

Here’s how to define the addCollectionsOption type with a generic schema:

type addCollectionsOptions<'schema> = {schema: 'schema}

The generic type is named 'schema. We don’t know anything about this type except that it exists. Note that the 'schema name only takes effect within this type. That means it’s totally different from the 'schema that we used as a generic on the Schema.json external.

Yes, I could have made that less ambiguous by usinga different variable name, but there’s actually method to this madness. You see, the Schema.json external does not actually have a specific enough type. We won’t be able to pass its values into our Rescript functions without giving more information about those values. Specifically, we need to know that it is an object containing two keys: recipes and tags, though we don’t need to model what types the values are for those keys.

Change the Schema.json external as follows:

@module("./Schema.json") external schema: {"recipes": 'schema, "tags": 'schema} = "default"

The type is all that’s changed, but there are a couple things to notice about that type:

  • It is a Rescript object not a record. The difference in syntax is as subtle as the difference in meaning. I’ll leave the official docs to explain the difference in meaning. The difference in syntax is that in records, the keys are not surrounded with quotes and in objects they are. In general, records are more type safe, but objects are easier for certain kinds of JS interop. Since we are modelling a complex JS interop, I chose objects.
  • Because we don’t care what type the object field are, we assign them a generic type. It is still named 'schema. In fact, the 'schema type is still, from Rescript’s perspective, completely different from the 'schema we used in addCollectionsOptions. However, from our perspective, as coders, they are now referring to the same type of thing.

Modeling the addCollections method

It took a bit of research to figure out how addCollections behaves. It accepts a dictionary of collections options and returns a promise with the newly added collections as values. However, I realized we don’t really need that return value; I’m just going to discard it. This means I can model it as a generic type inside the promise.

On the javascript side, this is a method, so we use @send to model it, like we did with destroy. The first argument is the object that the method will be called on, which is the database itself (the t in our module).

So here’s the entire external declaration:

@send
external addCollections: (t, Js.Dict.t<addCollectionsOptions<'schema>>) => Promise.t<'collections> =
  "addCollections"

It’s only two lines of code, but a bit complicated to read. Break it down into @send external modifier, addCollections name, the function type, and the name of the addCollections method on the object being modelled (the RxDatabase, t) to make sure you understand it.

Constructing the options and calling the method

Now we can update the database constructor to use Js.Dict to construct a dictionary mapping collection names as keys to addCollectionsOptions objects as values. Those options objects will have only one field, for now: the schema as a generic type. We’ll look up the schema on the schema object from the Schema.json external.

We can’t call the method until we actually have a database object, so this work happens inside the then handler on the promise returned by createRxDatabase.

First construct an empty options dict and set two fields on it (shown here with the rest of the make method for context):

let make: unit => Promise.t<t> = () => {
  createRxDatabase({name: "recipes", adapter: "idb"})->Promise.then(db => {
    let options = Js.Dict.empty()
    options->Js.Dict.set("recipes", {schema: schema["recipes"]})
    options->Js.Dict.set("tags", {schema: schema["tags"]})
  })
}

Unlike Belt.Map.String, which we discussed several articles ago, the empty value on a Js.Dict is a function. The returned dictionary is a mutable javascript object, so it needs to generate a new one for every copy of the dictionary. Under the hood, this maps to a very simple var options = {} in Javascript.

The Js.Dict.set function takes an options as the first argument (via ->), a key and the value. The value is constructed dynamically as an object with a schema key and the vasue looked up in the schema dict from Schema.json.

Calling our addCollections function is a one-liner:

db->addCollections(options)->Promise.then(_ => Promise.resolve(db))

We pipe the db into the externally declared addCollections function, passing in the options we just constructed.

The resulting promise is piped into Promise.then. We are discarding the returned value (with the _ default name). Instead, we return a resolved Promise with the db. If you unwrap everything, this is the promise that returns a value from make, so it needs to fulfill makes type signature.

How did I know I needed to do that? Rescript pointed out a type error between what I was returning and what I thought I was returning. This would not have been caught by Javascript code. To be fair, it would be a bit simpler in Javascript because I would be using async/await syntax.

If you are still logging the db in App.res, you can open it in the dev tools console and should see the two collections under the collections property.

Conclusion

I just realized this is getting rather long and I better split it out to multiple articles. We haven’t actually written a ton of code to get to where we are, but there was a lot to understand when it comes to modelling JS in Rescript. Once you understand it, however, it’s a lightweight process that is easy to do ad-hoc.

In the next article, we’ll model actual collections and build RxDB queries for our offline-database.

The commits for this article can be found in the db-connection branch.