cyclejs/cycle-core

View on GitHub
docs/drivers.html

Summary

Maintainability
Test Coverage
<!doctype html>
<html>

<head>
  <meta charset='utf-8'>
  <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
  <meta name="viewport" content="width=device-width">
  <title>Cycle.js - Drivers</title>

  <!-- Flatdoc -->
  <script src='support/vendor/jquery.js'></script>
  <script src='support/vendor/highlight.pack.js'></script>
  <script src='legacy.js'></script>
  <script src='flatdoc.js'></script>

  <!-- Algolia's DocSearch main theme -->
  <link href='//cdn.jsdelivr.net/docsearch.js/2/docsearch.min.css' rel='stylesheet' />

  <!-- Others -->
  <script async src="//static.jsbin.com/js/embed.js"></script>

  <!-- Flatdoc theme -->
  <link href='theme/style.css' rel='stylesheet'>
  <script src='theme/script.js'></script>
  <link href='support/vendor/highlight-github-gist.css' rel='stylesheet'>

  <!-- Meta -->
  <meta content="Cycle.js - Drivers" property="og:title">
  <meta content="A functional and reactive JavaScript framework for predictable code" name="description">

  <!-- Content -->
  <script id="markdown" type="text/markdown" src="index.html">
# Drivers

## Plugins for effects

Throughout this documentation site we have extensively used *drivers*. The DOM Driver has been the most common one, but also the HTTP driver was used.

What are drivers and when should you use them? When should you create your own driver, and how do they work? These are a few questions we will address in this chapter.

**Drivers are functions that listen to sink streams (their input), perform imperative side effects, and may return source streams (their output).**

They are meant for encapsulating imperative side effects in JavaScript. The rule of thumb is: whenever you have a JavaScript function such as `doSomething()` which returns nothing, it should be contained in a driver.

Let's study what drivers do by analyzing the most common one: the DOM Driver.

> ### Why the name "driver"?
>
> In Haskell 1.0 Stream I/O, similar in nature to Cycle.js, there is a cyclic interaction between the program's `main` function and Haskell's `os` function. In operating systems, drivers are software interfaces to use some hardware devices, which incur side effects in the external world.
>
> In Cycle.js, one can consider the "operating system" to be the execution environment surrounding your application. Roughly speaking, the DOM, the console, JavaScript and JS APIs assume the role of the operating system for the web. We need *software adapters* to interface with the browser and other environments such as Node.js. Cycle.js drivers are there as adapters between the external world (including the user and the JavaScript execution environment) and the application world built with Cycle.js tools.

## DOM Driver

The DOM Driver is the most important and most common driver in Cycle.js. When building interactive web apps, it is probably the most important tool in Cycle.js. In fact, while Cycle *Run* function is only about 200 lines of code, Cycle *DOM* is at least 4 times larger.

Its main purpose is to be a proxy to the user using the browser. Conceptually we would like to work assuming the existence of a `human()` function, as this diagram reminds us:

![Human computer diagram](img/human-computer-diagram.svg)

However, in practice, we write our `main()` function targeted at a `domDriver()`. For a user interacting with a browser, we only need to make our `main()` interact with the DOM. Whenever we need to show something to the user, we instead show that to the DOM, and the DOM together with the browser shows that to our user. When we need to detect the user's interaction events, we attach event listeners on the DOM, and the DOM will notify us when the user interacts with the browser on the computer.

![Main, DOM Driver, Side Effects](img/main-domdriver-side-effects.svg)

Notice there are two directions of interaction with the external world through the DOM. The *write* effect is the renderization of our Snabbdom VNodes to DOM elements which can be shown on the user's screen. The *read* effect is the detection of DOM events generated by the user manipulating the computer.

The `domDriver()` manages these two effects while allowing them to be interfaced with the `main()`. The *input* to `domDriver()` captures instructions for the *write* effect, and the *read* effect is exposed as the *output* of `domDriver()`. The anatomy of the `domDriver()` function is roughly the following:

```javascript
function domDriver(vdom$) {
  // Use vdom$ as instructions to create DOM elements
  // ...
  return {
    select: function select(selector) {
      // returns an object with two functions: `events()`
      // and `elements()`. Function `events(eventType)`
      // returns the stream of `eventType` DOM events
      // happening on the elements matched by `selector`.
      // Function `elements()` is the stream of DOM
      // elements matching the given `selector`.
    }
  };
}
```

The input `vdom$` is the output from `main()`, and the output of `domDriver()` is the input to `main()`:

```javascript
function main(sources) {
  // Use sources.DOM.select(selector).events(eventType)
  // ...
  // Create vdom$ somehow
  // ...
  return {
    DOM: vdom$
  };
}
```

As a recap:

- `main()`: takes **sources** as input, returns **sinks**
- `domDriver()`: takes **sinks** as input, performs write and read effects, returns **sources**.

## Isolating side effects

Drivers should always be associated with some I/O effect. As we saw, even though the DOM Driver's main purpose is to represent the user, it has write and read effects.

In JavaScript, nothing stops you from writing your `main()` function with effects. A simple `console.log()` is already an effect. However, to keep `main()` pure and reap its benefits like testability and predictability, it is better to encapsulate all I/O effects in drivers.

Imagine, for instance, a driver for network requests. By isolating the network request effect, your application's `main()` function can focus on business logic related to the app's behavior, and not on lower-level instructions to interface with external resources. This also allows a simple method for testing network requests: you can replace the actual network driver with a fake network driver. It just needs to be a function that mimics the network driver function, and makes assertions.

Avoid making drivers if they do not have effects to the external world somehow. Especially do not create drivers to contain business logic. This is most likely a code smell.

Drivers should focus solely on being an interface for effects, and usually are libraries that simply enable your Cycle.js app to perform different effects. Sometimes, though, a one-liner driver can be created on the fly instead of being a library, for instance this simple logging driver:

```javascript
run(main, {
  log: msg$ => { msg$.addListener({next: msg => console.log(msg)}) }
});
```

## Read-only and write-only

Most drivers, like the DOM Driver, take *sinks* (to describe a *write*) and return *sources* (to catch *reads*). However, we might have valid cases for write-only drivers and read-only drivers.

For instance, the one-liner `log` driver we just saw above is a write-only driver. Notice how it is a function that does not return any stream, it simply consumes the sink `msg$` it receives.

Other drivers only create source streams that emit events to the `main()`, but don't take in any `sink` from `main()`. An example of such would be a read-only WebSocket driver, drafted below:

```javascript
function WSDriver(/* no sinks */) {
  return xs.create({
    start: listener => {
      this.connection = new WebSocket('ws://localhost:4000');
      connection.onerror = (err) => {
        listener.error(err)
      }
      connection.onmessage = (msg) => {
        listener.next(msg)
      }
    },
    stop: () => {
      this.connection.close();
    },
  });
}
```

## How to make drivers

You should only be reading this section if you have clear intentions to make a driver and expose it as a library (unless it's a one-liner driver). Typically, when writing a Cycle.js app, you do not need to create your own drivers.

Consider first carefully which effects your driver is responsible for. And can it have both read and write effects?

Once you map out the read/write effects, consider how diverse those can be. Create an empathic API which covers the common cases elegantly.

The **input** to the driver function is expected to be a single `xstream` stream. This is a practical API for the app developer to use when returning the `sinks` object in `main()`. Notice how the DOM Driver takes a single `vdom$` stream as input, and how sophisticated and expressive VNodes from Snabbdom can be. On the other hand, don't always choose JavaScript objects as the values emitted in the Observable. Use objects when they make sense, and remember to keep the API simple rather than overly-generic. Don't over-engineer.

Note that even if you are using RxJS and `@cycle/rxjs-run`, the sink given to the driver is always an `xstream` Stream. You may need to convert from xstream to RxJS in the driver code, with `Rx.Observable.from(sink$)`. This is because xstream is powering Cycle.js internally.

As a second, optional, argument to the driver function, you can expect `name`. This is the name that was given to the driver in the `drivers` object in `run(main, drivers)`. For instance, the DOM Driver usually gets the name `DOM`. Typically, drivers don't need to use this argument, but it is available anyway. This is the expected signature for a driver function:

```javascript
function myDriver(sink$, name /* optional */)
```

The **output** of the driver function can either be a single stream or a *queryable collection* of streams.

In the case of a single stream as output source, depending on how diverse the values emitted by this stream are, you might want to make those values easily filterable (using the xstream or RxJS or Most.js `filter()` operator). Design an API which makes it easy to filter the stream, keeping in mind what was provided as the sink stream to the driver function.

In some cases it is necessary to output a queryable collection of streams, instead of a single one. A **queryable collection of streams** is essentially a JavaScript object with a function used to choose a particular stream based on a parameter, e.g. `get(param)`.

The DOM Driver, for instance, outputs a queryable collection of streams. The collection is in fact lazy: none of the streams outputted by `select(selector).events(eventType)` existed prior to the call of `events()`. This is because we cannot afford creating streams for *all* possible events on *all* elements on the DOM. Take inspiration from the lazy queryable collection of streams from the DOM Driver whenever the output source contains a large (possibly infinite) amount of streams.

In order to make your driver usable with many stream libraries, you should use the `adapt()` function from `@cycle/run/lib/adapt` to convert the stream to the same library used for `run`. `adapt()` takes an **xstream** stream as input and returns a stream for the library used in `run`.

```typescript
adapt(stream: xs.Stream<T>): xs.Stream<T>; // for @cycle/run
adapt(stream: xs.Stream<T>): Rx.Observable<T>; // for @cycle/rxjs-run
adapt(stream: xs.Stream<T>): most.Stream<T>; // for @cycle/most-run
```

Note that `adapt` is always imported from as `@cycle/run/lib/adapt`, **not** from `@cycle/rxjs-run/lib/adapt` nor `@cycle/most-run/lib/adapt`. Before returning a single-stream source from the driver, make sure to call `adapt`:

```js
import {adapt} from '@cycle/run/lib/adapt';

function WSDriver(/* no sinks */) {
  const source = xs.create({
    start: listener => {
      this.connection = new WebSocket('ws://localhost:4000');
      connection.onerror = (err) => {
        listener.error(err)
      }
      connection.onmessage = (msg) => {
        listener.next(msg)
      }
    },
    stop: () => {
      this.connection.close();
    },
  });

  return adapt(source);
}
```

If you return *queryable collection of streams*, make sure that each method which returns a stream calls `adapt()` as well.

It is usually better to write drivers in xstream, since you don't need to convert the sink, and given xstream's tiny size as a dependency.

## Example driver

Suppose you have a fake real-time channel API called `Sock`. It is able to connect to a remote peer, send messages, and receive push-based messages. The API for `Sock` is:

```javascript
// Establish a connection to the peer
let sock = new Sock('unique-identifier-of-the-peer');

// Subscribe to messages received from the peer
sock.onReceive(function (msg) {
  console.log('Received message: ' + msg);
});

// Send a single message to the peer
sock.send('Hello world');
```

**How do we build a driver for `Sock`?** We start by identifying the effects. The *write* effect is `sock.send(msg)` and the *read* effect is the listener for received messages. Our `sockDriver(sink)` should take `sink` as instructions to perform the `send(msg)` calls. The output from `sockDriver()` should be `source`, containing all received messages.

Since both input and output should be streams, it's easy to see `sink` in `sockDriver(sink)` should be an stream of outgoing messages to the peer. And conversely, the source should be a stream of incoming messages. This is a draft of our driver function:

```javascript
import {adapt} from '@cycle/run/lib/adapt';

function sockDriver(outgoing$) {
  outgoing$.addListener({
    next: outgoing => {
      sock.send(outgoing);
    },
    error: () => {},
    complete: () => {},
  });

  const incoming$ = xs.create({
    start: listener => {
      sock.onReceive(function (msg) {
        listener.next(msg);
      });
    },
    stop: () => {},
  });

  return adapt(incoming$);
}
```

The listener of `outgoing$` performs the `send()` effect, and the returned stream `incoming$` is based on `sock.onReceive` to take data from the external world. However, `sockDriver` is assuming `sock` to be available in the closure. As we saw, `sock` needs to be created with a constructor `new Sock()`. To solve this dependency, we need to create a factory that makes `sockDriver()` functions.

```javascript
import {adapt} from '@cycle/run/lib/adapt';

function makeSockDriver(peerId) {
  let sock = new Sock(peerId);

  function sockDriver(outgoing$) {
    outgoing$.addListener({
      next: outgoing => {
        sock.send(outgoing);
      },
      error: () => {},
      complete: () => {},
    });

    const incoming$ = xs.create({
      start: listener => {
        sock.onReceive(function (msg) {
          listener.next(msg);
        });
      },
      stop: () => {},
    });

    return adapt(incoming$);
  }

  return sockDriver;
}
```

`makeSockDriver(peerId)` creates the `sock` instance, and returns the `sockDriver()` function. We use this in a Cycle.js app as such:

```javascript
function main(sources) {
  const incoming$ = sources.sock;
  // Create outgoing$ (stream of string messages)
  // ...
  return {
    sock: outgoing$
  };
}

run(main, {
  sock: makeSockDriver('B23A79D5-some-unique-id-F2930')
});
```

Notice we have the `peerId` specified when the driver is created in `makeSockDriver(peerId)`. If the `main()` needs to dynamically connect to different peers according to some logic, then we shouldn't use this API anymore. Instead, we need the driver function to take instructions as input, such as "connect to peerId", or "send message to peerId". This is one example of the considerations you should take when designing a driver API.

## Extensibility

Cycle *Core* is a very small framework, and Cycle *DOM*'s Driver is available as an optional plugin for your app. This means it is simple to replace the DOM Driver with any other driver function providing interaction with the user.

You can for instance fork the DOM Driver, adapt it to your preferences, and use it in a Cycle.js app. You can create a driver to interface with sockets. Drivers to perform network requests. Drivers meant for Node.js. Drivers that target other UI trees, such as `<canvas>` or even native mobile UI.

As a framework, it cannot be compared to monoliths which have ruled web development in the recent years. Cycle.js itself is after all just a small tool and a convention to create reactive dialogues with the external world using reactive streams.

  </script>

  <!-- Initializer -->
  <script>
    Flatdoc.run({
      fetcher: function (callback) {
        callback(null, document.getElementById('markdown').innerHTML);
      },
      highlight: function (code, value) {
        return hljs.highlight(value, code).value;
      },
    });
  </script>

</head>

<body role='flatdoc' class="no-literate">

  

  <div class='header'>
    <div class='left'>
      <h1><a href="/"><img class="logo" src="img/cyclejs_logo.svg">Cycle.js</a></h1>
      <ul>
        <li><a href='getting-started.html'>Guide</a></li>
        <li><a href='api/index.html'>API</a></li>
        <li><a href='releases.html'>Releases</a></li>
        <li><a href='https://github.com/cyclejs/cyclejs'>GitHub</a></li>
      </ul>
      <input id="docsearch" />
    </div>
    <div class='right'>
      <!-- GitHub buttons: see https://ghbtns.com -->
      <iframe src="https://ghbtns.com/github-btn.html?user=cyclejs&amp;repo=cyclejs&amp;type=watch&amp;count=true"
        allowtransparency="true" frameborder="0" scrolling="0" width="110" height="20"></iframe>
    </div>
  </div>

  <div class='content-root'>
    <div class='menubar'>
      <div class='menu section'>
        
        <ul>
          
          <li><a href="getting-started.html" class="level-1 out-link">Getting started</a></li>
          
          <li><a href="dialogue.html" class="level-1 out-link">Dialogue abstraction</a></li>
          
          <li><a href="streams.html" class="level-1 out-link">Streams</a></li>
          
          <li><a href="basic-examples.html" class="level-1 out-link">Basic examples</a></li>
          
          <li><a href="model-view-intent.html" class="level-1 out-link">Model-View-Intent</a></li>
          
          <li><a href="components.html" class="level-1 out-link">Components</a></li>
          
        </ul>
        
        <div role='flatdoc-menu'></div>
        
      </div>
    </div>
    <div role='flatdoc-content' class='content'></div>
    
  </div>
  

  <script>
    ((window.gitter = {}).chat = {}).options = {
      room: 'cyclejs/cyclejs'
    };
  </script>
  <script src="https://sidecar.gitter.im/dist/sidecar.v1.js" async defer></script>
  <script src='//cdn.jsdelivr.net/docsearch.js/2/docsearch.min.js'></script>
  <script src='docsearch.js'></script>
</body>

</html>