Skip to content

Creating a new Transporter or ClientStorage implementation

Johannes Merz edited this page Oct 26, 2016 · 7 revisions

The transfer of data to remote backends and/or local databases in harmonized.js is based on "Transporters". A transporter is the link between the item stores and the outside world. The transporter-system of harmonized was created with extendability in mind. This way it is possible to create your own transporters for technology we don't already cover (for example you could build an Electron app that uses some local database that you want to work with harmonized).

So, how are transporters build in harmonized?

The architecture of transporters

harmonized stores know two kind of transporters:

  • Actual Transporters that connect to an endpoint over the network
  • and ClientStorages that connect to a local database.

You can omit one or even both by using the EmptyTransporter (name not final) instead of a real implementation.

Transporters and ClientStorages are really similar. They even share the BaseTransporter, that takes care of the API that the store uses and the queue system to handle outstanding requests to the respective endpoints (local database or server). The only real difference from an API standpoint is that ClientStorages have a remove() method to mark an item as removed until the item is deleted from the Transporter.

The API to implement in a new Transporter

There are only a few methods and properties that you need to implement:

  • _prepareSend(queueItem)
  • _send(requestOptions)
  • _prepareFetch(...args)
  • _fetch(options)
  • _prepareFetchOne(...args)
  • _sendFetchOne(options)
  • _prepareInitialFetch(...args)
  • _initialFetch(options)

The prepare methods are there to create the initial requestOptions that are then modified through the "send" middleware. After the middleware have modified the requestOptions the corresponding send/fetch method is called with this requestOptions.

To get a better idea of what these methods do, here are more detailed descriptions:

_prepareSend(queueItem: PushQueueItem): Object

This method takes a queue item that needs to be send to the endpoint of the transporter and returns the request options object in the format the _send() function needs (like the requestOptions for the http fetch() function). The PushQueueItem has only two important properties:

  • payload (Object): the content of the item to send to the server
  • action (string): the action to perform (can be "create", "update" or "delete")

According to the action you need to create the correct options for the _send() method. To make this more clear here is an example for a HTTP transporter: Is the action "create", you use the HTTP method POST. On "update" you use PUT and for "delete" you use DELETE.

The payload should wander in the body/payload section of your requestOptions. But you could also modify the payload if you need to (IMPORTANT: if you change the payload, make sure you create a deep clone of it payload first, otherwise this could lead to some undesired side effects).

_send(requestOptions: Object): Promise

Sends the previously prepared item to the server/database. Here the actual connection to the endpoint is called (for example fetch() for a HTTP endpoint). This should return a Promise that is resolved when the send request to the endpoint was successful and rejected when an error happened during sending.

_prepareFetch(...args: any): Object

Prepares the request options for the _fetch() method. Here the "send" middleware also modifies the returned options before sending. Unlike the send() method this doesn't take a PushQueueItem but you rather can define as many custom arguments you need or like (these are passed through from the fetch() method from the store).

_fetch(requestOptions: Object): Promise

Like the _send() method this actually calls the server/database implementation to fetch the data. Also this returns a Promise that is resolved or rejected depending on the success of the server/database call.

_prepareFetchOne(itemInformation: number | Object, ...args: any): Object

Like _prepareFetch(), only for _fetchOne(). The only difference is that the first argument is either the ID (of the endpoint) of the requested item or the item itself that needs to be fetched again to keep it up-to-date. The other args are again free to use.

_fetchOne(requestOptions: Object): Promise

Like the _fetch() method, but only fetches one specific item.

_prepareInitialFetch(... args: any): Object

Like _prepareFetch(), only for _initialFetch().

_initialFetch(requestOptions: Object): Promise

This method is called when the app starts and the state of the store is empty at first. You can use this to catch everything at the start of the app and use the _fetch() method to only fetch updates. If you don't want this to be different from _fetch() just call the _fetch() method, like this:

_initialFetch(requestOptions) {
  return this._fetch(requestOptions);
}

Properties

Besides these methods your transporter most of the time needs an own independent copy of the static middleware of it's superclass. Because of this you need to clone the static middleware property of the superclass of your transporter.

Here is an example for a custom ClientStorage:

class IndexedDbClientStorage extends ClientStorage {
  static middleware = [...ClientStorage.middleware];
  // ...
}

If you don't want to use the static keyword, you can just append it to the class from the outside:

class IndexedDbClientStorage extends ClientStorage {
  // ...
}

IndexedDbClientStorage.middleware = ClientStorage.middleware.splice();

We heavily suggest to clone the middleware! Otherwise your transporter would just share the middleware with its superclass. This can cause undesired side effects (like using middleware from other transporters that don't work together). But in some cases it could be desired to share your middleware with the superclass. For example when you want to create a more sophisticated HTTP transporter that extends the HttpTransporter and you have the same interface for the input and output of the middleware. In this case you don't need to do anything, after extending it will automatically share the middleware with its superclass!