redux-sagaのchannelで擬似的な排他制御を実装してみる

# はじめに
React+redux+redux-sagaのアプリケーション開発の中で排他制御を行う必要がでてきたので、　redux-sagaのchannelの利用して排他制御っぽくできないかを検討しました。この記事はその結果をまとめたものです。

今回の検討で利用した主なライブラリのバージョンは以下になります。

- redux: 4.0.4
- redux-saga: 1.0.5

# 背景
排他制御が出てきたのは、以下の一連の流れを排他的に実行したいという要件が出てきたからでした。

1. reduxのstoreから値を取得する
2. 取得した値を元に加工する
3. 加工した値でstoreを更新する

この一連の処理を実行するredux-sagaのtaskを作成し、このtaskの同時実行数を制御することで排他制御を実現できないかと考えました。

# redux-sagaのchannel
公式APIリファレンスは[こちら](https://redux-saga.js.org/docs/api/#channelbuffer)
[channelの使い方についてのページ](https://redux-saga.js.org/docs/advanced/Channels.html)もあります。

ざっくりと説明すると、channelはredux-sagaのtake作用と同様にactionを待ち受けてタスクを起動することができます。channelは単純にtakeで待ち受けるのと違い、起動したタスクが終了していない状態で新たにactionがdispatchされた場合一旦actionをbufferに格納します。タスクが終了後、bufferにある一番古いactionを取り出して再びタスクを起動します。

# サンプルコード
サンプルコードの全体は[こちら](https://github.com/r-nouchi/redux-saga-example/tree/feature/task-exclusion-control)

サンプルは以前の記事でも使用したものを利用して、さらに機能を追加しています。
`selection` と表示されたボタンをクリックするとActionがdispatchされます。

![スクリーンショット 2019-11-04 20.54.59.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/306956/6d18b8f7-57c2-5cc4-a55b-f17ce4f18800.png)

```selection.jsx
import React from 'react'

const selection = (props) => (
  <div>
    <button type="button" onClick={props.onClickSelectionButton}>selection</button>
  </div>
)

export default selection
```

```selectionContainer.js
import { connect } from 'react-redux'
import selection from '../component/selection'
import {
  selectionStartEvent,
} from '../action'

let count = 0

const mapStateToProps = (state) => ({
  sampleState: state.selectionState,
})

const mapDispatchToProps = (dispatch) => ({
  onClickSelectionButton: () => {
    dispatch(selectionStartEvent(count++))
  },
})

export default connect(
  mapStateToProps,
  mapDispatchToProps
)(selection)
```

続いてState。データの加工対象となる数値が入った配列と、使用済みの数値を入れる配列の二つを持っています。

```selectionState.js
import {
  SELECTION_SUCCESS_EVENT,
} from '../action'

const initialState = {
  source: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
  selected: [],
}

export default (state = initialState, action) => {
  switch(action.type) {
    case SELECTION_SUCCESS_EVENT:
      return {
        ...state,
        selected: [...state.selected, action.payload.selected],
      }
    default:
      return state
  }
}
```

そして肝となるsaga。
1. storeからデータを取得する
2. source配列からselected配列にあるものを除く
3. 残ったものを降順にソートして一番大きいものを次のactionのpayloadとして渡す
4. reducerにより、actionのpayloadで渡された値がselected配列に格納される

という処理を行なっています。この処理を排他的に実行しなかった場合、storeからのデータ取得〜store更新までの間に同じactionがdispatchされると更新前のstoreを取得してしまい同じ値がselected配列に格納されてしまいます。排他制御できているかをわかりやすくするために３秒間スリープさせています。

```selectionSaga.js
import {
  take,
  call,
  put,
  select,
  actionChannel,
} from 'redux-saga/effects'
import {
  buffers,
} from 'redux-saga'
import {
  SELECTION_START_EVENT, selectionSuccessEvent,
} from '../action'

function* handleSelectionStart() {
  const channel = yield actionChannel(SELECTION_START_EVENT, buffers.expanding(4))
  while(true) {
    const action = yield take(channel)
    console.log("take action: %o", action)
    const state = yield select(state => state.selectionState)
    console.log("selectionState: %o", state)
    const selected = yield call(selectionAsync, state.source, state.selected)
    const successAction = yield put(selectionSuccessEvent(selected))
    console.log("task finished. dispatchedAction: %o", successAction)
  }
}

const selectionAsync = async (source, exclude) => {
  console.log("slection start. source: %o exclude: %o", source, exclude)
  const excludedSource = source.filter(value => !exclude.includes(value))
  console.log("excludedSource: %o", excludedSource)
  const sortedSource = excludedSource.sort((a, b) => b - a)
  console.log("selection end. sortedSource: %o", sortedSource)
  console.log("sleep start")
  await new Promise(r => setTimeout(r, 3000))
  console.log("sleep end")
  return sortedSource[0]
}

export default [
  handleSelectionStart,
]
```

実際に動作させてみました。まずは単純に1回だけselectionボタンを押した場合。
![スクリーンショット 2019-11-04 20.51.48.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/306956/a6b6c2c8-d05a-d348-a623-c5b3fab8116c.png)

一番大きい値である`9`がpayloadに渡されているのがわかります。
続いて2回短い感覚でselectionボタンを連打します。
![スクリーンショット 2019-11-04 20.54.25.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/306956/bdbd3fd6-dbd2-3f13-2897-13417b9ca7a2.png)

actionは短い間隔で連続でdispatchされますが、sagaの処理は順番に処理されていっています。最後のactionのpayloadが`7`になっているのでちゃんと排他制御されているのがわかります。

# channelのbufferの挙動
sagaのcahnnelを作成する部分でbufferの挙動を制御することができます。
`const channel = yield actionChannel(SELECTION_START_EVENT, buffers.expanding(4))`

今回は初期サイズ4で、初期サイズを上回るactionがdispatchされた場合は動的にサイズを拡張していくbufferを設定しました。
他にどのような設定ができるかは[公式のAPIリファレンス](https://redux-saga.js.org/docs/api/#buffers)を参照してください。

# まとめ
sagaのchannelを利用することで排他制御を実装することができました。
今回は排他制御が目的だったためchannelで起動するタスクの同時起動数を1にしましたが、forkと組み合わせることで任意の数で同時起動数を制御することもできます。こちらは性能面、流量制御で有用かと思います。こちらに関しても機会があればまとめてみたいと思います。