Worker APIで状態管理するライブラリ Businessman を書いた

[Worker(➚MDN)](https://developer.mozilla.org/ja/docs/Web/API/Worker/Worker) をつかって状態管理するライブラリ [Businessman(➚GitHub)](https://github.com/businessmanjs/businessman) を作りました。

JavaScript はシングルスレッドが基本ですが、Businessman では Worker によって、UI を操作するメインスレッドとは別のスレッドで状態管理をします。

マルチスレッドによって、高負荷な処理があっても UI の応答性を確保することができます。

また、ステートをアプリケーションが直接操作する危険性も排除できます。

# TL;DR

Businessman を利用する手順は以下のようになります。

- `npm install --save businessman` でインストールする
- Worker にインストールするためのファイルをビルドする
- `install( '/path/to/worker.js' )` で状態管理を開始する

このあとは `dispatch` や `subscribe` などのよくある API をつかって状態を更新したり取得することができます。

![Businessman](https://raw.githubusercontent.com/businessmanjs/businessman/master/public/assets/businessman.png)

# Worker を作成する

ほかの状態管理ライブラリと最も異なるのが、状態管理用のファイルをアプリケーションとは別のファイルに持つ必要があるということです。

なぜなら、`Worker()` コンストラクタでは以下のように外部スクリプトを引数に取り Worker インスタンスを作成するからです。

```js
var myWorker = new Worker( 'worker.js' )
```

## Overview

Worker 用のファイルを作成するための最小の記述は、例として以下のようになります。

```js:worker.js
import { worker } from 'businessman'

worker.registerStore( {
    type: 'counter',
    state: 0,
    mutations: {
        increment: ( state, num ) => {
            return state += num
        }
    },
    actions: {
        increment: ( commit, num = 1 ) => {
            commit( 'increment', num )
        }
    },
    getters: {
        absolute: ( state ) => {
            return Math.abs( state )
        }
    }
} )

worker.start()
```

詳しく見ていきます。

## worker.registerStore

ストアを登録します。

引数には、`type`, `state`, `mutations`, `actions` を含んだオブジェクトをとります。

### type

`type` はストアのアイデンティティになります。Businessman では **ひとつのストアにひとつの状態** を持つことを推奨[^smallstate] しているので、`type` は状態の名前とも言えます。

[^smallstate]: スレッド間のデータサイズがパフォーマンスに影響するためです。

```js:ex
type: 'counter'
```

> `type` は必ずユニークな文字列である必要があります。
> ほかのストアと同じ値は使用できません。

### state

`state` はストアの状態を保持します。`registerStore` の際には初期値を指定しておきます。値はどのような型でも使えるはずです。

```js:ex
state: 0
```

### mutations

`mutations` は現在の状態とアクションからのデータを受け取って、新しい `state` を返します。

その後、自動的に状態が変更されてメインスレッドに通知されます。

```js:ex
increment: ( state, num ) => {
    return state += num
}
```

> ミューテーションには非同期処理を置くことができません。

### actions

`actions` は `mutations` のミューテーションを呼び出すための関数です。

第 1 引数の `commit` を実行することでミューテーションを呼び出します。

アクションでは REST API を呼び出して新しいデータを取得するなど、非同期処理を置くこともできます。

```js:ex
increment: ( commit, num = 1 ) => {
    commit( 'increment', num )
}
```

コミットするとメインスレッドに状態が通知されますが、第 3 引数を `false` にすると状態を `null` として通知することができます。

単純に状態が変わったという事実だけが知りたい場合には、より高速に処理するために有用です。

```js:ex
increment: ( commit, num = 1 ) => {
    commit( 'increment', num, false )
}
```

### Getters

後述する `getState()` で状態を取得するときに、任意の算出方法によって状態を取得できます。

```js:ex
absolute: ( state ) => {
    return Math.abs( state )
}
```

## worker.registerManager

アクションとミューテーションはひとつのストアに所属するため、複数のストアを横断した処理を書くことができません。そのときは `manager` をつかうことで実現できます。

`manager` は `worker.registerManager` を使って登録します。

```js:ex
worker.registerManager( {
    type: 'countUpMessage',
    handler: ( stores, num = 1 ) => {
        stores.counter.dispatch( 'increment', num )
        stores.message.dispatch( 'update', `${num} has been added to the counter` )
    }
} )
```

詳しく見ていきます。

### type

`type` はマネージャーのアイデンティティです。ストア同様、ユニークな文字列である必要があります。他のマネージャーと同じ値は使用できません。

```js:ex
type: 'countUpMessage'
```

### handler

`handler` はマネージャーで実行したい処理を書いた関数です。

第 1 引数にはすべてのストアインスタンスが入ってくるので、処理したい内容に応じて `dispatch`( 後述 )を呼び出します。

```js:ex
handler: ( stores, num = 1 ) => {
    stores.counter.dispatch( 'increment', num )
    stores.message.dispatch( 'update', `${num} has been added to the counter` )
}
```

## worker.start

ストア( と任意でマネージャー )を登録したら、その Worker がインストールされたときに状態管理を開始する必要があります。

```js:ex
worker.start()
```

とくにオプションはありません。これだけです。

## ビルド

最後に、ここまで新しい ES の仕様に合わせて記述してきましたが、このままでは **ブラウザでは動きません** 。

Rollup や Babel などを使ってブラウザ向けに変換しておいてください。

個人的には Rollup + [Buble(➚npm)](https://www.npmjs.com/package/buble) の組み合わせをよく使います。

---------

# Worker をインストールする

Worker ができたら、今度はインストールします。ここからは UI スレッドの領域になります。

```js:ex
import { install } from 'businessman'

install( '/path/to/worker.js' )
```

インストールは非同期ですが、インストールが終わる前から、後述する `dispatch` や `subscribe` を実行できます。

# 状態の操作

ストアの操作や購読をします。

~~まずは標準の方法を書きますが、より[馴染み深いスタイル](#%E3%82%B9%E3%83%88%E3%82%A2%E3%82%B9%E3%82%BF%E3%82%A4%E3%83%AB%E3%81%AB%E3%82%88%E3%82%8B%E6%93%8D%E4%BD%9C)での使い方も後述しています。~~

## dispatch

ストアのアクションを実行します。

第 1 引数にストアタイプ、第 2 引数にアクションタイプ、第 3 引数にアクションに渡したい値を指定します。

```js:ex
import { dispatch } from 'businessman'

dispatch( 'counter', 'increment', 1 )
```

## subscribe

ストアの更新を受け取ります。

第 1 引数にストアタイプ、第 2 引数には更新を受け取ったときのコールバックを指定します。

コールバックには更新後の新しい状態と、適用されたミューテーションタイプが入ってきます。

```js:ex
import { subscribe } from 'businessman'

subscribe( 'counter', ( state, mutationType ) => {
    console.log( state, mutationType )
} )
```

## unsubscribe

サブスクライブを削除します。

第 1 引数にストアタイプを指定して、第 2 引数を指定すればひとつのコールバックだけを削除できます。デフォルトではすべてのコールバックを削除します。

```js:ex
import { unsubscribe } from 'businessman'

unsubscribe( 'counter' ) // すべてのコールバックを削除
unsubscribe( 'counter', listener ) // ひとつのコールバックだけを削除
```

> 無名関数がコールバックに指定されているときは、ひとつのコールバックだけを削除することはできません。

## operate

複数ストアを横断した処理をおこなうためのマネージャーを実行します。

マネージャータイプとペイロードを指定することで呼び出します。

```js:ex
import { operate } from 'businessman'

operate( 'countUpMessage', 1 )
```

## getState

ストアの状態を取得します。

ストアは Worker にあるため、状態の取得も非同期になります。

```js:ex
import { getState } from 'businessman'

getState( 'counter' )
.then( ( state ) => {
    console.log( state )
} )
```

Getters のタイプを指定して、計算後の状態を取得することもできます。

```js:ex
import { getState } from 'businessman'

getState( 'counter', 'absolute' )
.then( ( state ) => {
    console.log( state )
} )
```

## getAllState

すべてのストアの状態を取得します。

もちろん非同期になります。

```js:ex
import { getAllState } from 'businessman'

getAllState()
.then( ( state ) => {
    console.log( state )
} )
```

# ストアスタイルによる操作

**この API は v2.0.0 で廃止されました。利用する場合は v1.5.1 以下になります。**

`store.dispatch()` のような、よくあるスタイルで状態の操作をすることもできます。

ただし、ストアは Worker スレッドにあるため直接操作をすることはできません。

Businessman にビルトインされている `CREATE_CLIENT_STORE` アクションをサブスクライブすることによって、ストアスタイルによる操作をするためにストアのメソッドを一部コピーしたオブジェクトを受け取ることができます。

```js:ex
let counter

subscribe( 'CREATE_CLIENT_STORE', ( stores ) => {
    console.log( stores ) // { counter: { dispatch: function () {...}, subscribe: function () {...}, unsubscribe: function () {...}, getState: function () {...} } }
    counter = stores.counter
} )
```

`CREATE_CLIENT_STORE` から取得されるストアでは次のように API を呼び出します。

```js:ex
/* Dispatch */
counter.dispatch( 'increment', 1 )

/* Subscribe */
counter.subscribe( () => {
    ...
} )

/* Unsubscribe */
counter.unsubscribe()
// or
counter.unsubscribe( listener )

/* getState */
counter.getState()
.then( ( state ) => {
    ...
} )
```

ほかにも、`CREATE_CLIENT_MANAGER` をサブスクライブするとマネージャーの一覧を取得できます。

# これから

アップデートしていく予定です。状態のスナップショットと復元、ドキュメントの整備などなど。