【Rails】Action Cableについてまとめてみた

# Action Cableのしくみ

主に[Railsガイド](https://railsguides.jp/action_cable_overview.html)をなぞったもので、冗長なので後に修正したいです。

Action Cableは、フロントのWebSocketとバックエンドのRails周りをシームレスに統合する、フルスタックなフレームワーク。

***WebSocket***とは、Webにおいての双方向通信を、従来のHTTP等よりも低コストで行うための仕組み。

[WebSocketについて調べてみた。](https://qiita.com/south37/items/6f92d4268fe676347160)

## Pub/Subモデル(出版-購読型モデル)
非同期型メッセージパラダイムの1種で、***疎結合***とそれによるスケーラビリティが利点。
メッセージを送信する側(Publisher)が、特定のメッセージを受信する側(Subscriber)の抽象クラスに情報を送信する。このときPublisherは、個別の受信者を指定しない。
Action Cableは、***サーバーと多数のクライアント間の通信***にこのアプローチを採用している。

- [出版-購読型モデル](https://ja.wikipedia.org/wiki/%E5%87%BA%E7%89%88-%E8%B3%BC%E8%AA%AD%E5%9E%8B%E3%83%A2%E3%83%87%E3%83%AB)
- [Redisのpub/sub機能](https://qiita.com/jkr_2255/items/6ae9e5e2244052e41369)

## サーバーサイド(Rails側)コンポーネント
### 1. Connection
- サーバーでWebSocketを受け付けるたびに、Connectionオブジェクトがインスタンス化する。
- Connection自体は、アプリのロジックは扱わない。
- Connectionは`ApplicationCable::Connection`のインスタンスである。
- Connectionオブジェクトは、全てのChannel Subscriptionsの親となる(2-1)。
- ***Consumer***: WebSocket Connectionのクライアント。
- 各ユーザーが開くブラウザタブ、ウィンドウ、デバイスごとに、ConsumerのConnectionのペアが1つづつ作成される。

```app/channels/application_cable/connection.rb
module ApplicationCable
  class Connection < ActionCable::Connection::Base
  identified_by :current_user

  def connect
    self.current_user = find_verified_user
  end

  private
    def find_verified_user
      if verified_user = User.find_by(id: cookies.encryped[:user_id])
        verified_user
      else
        reject_unauthorized_connection
      end
    end
  end
end
```

### 2. Channel
- サーバーサイドのコンポーネントでは、Channelの設定が必要。
- ***MVCのコントローラ***と似ている。
- Railsはデフォルトで、Channel間で共有されるロジックをカプセル化する`ApplicationCable::Channel`という親クラスを作成する。

#### 2-1.親Channelの設定
```app/channels/application_cable_channel.rb
module ApplicationCable
  class Channel < ActionCable::Channel::Base
  end
end
```

```app/channel/chat_channel.rb
class ChatChannel < ApplicationCable::Channel
end
```

```app/channels/appearance_channel.rb
class AppearanceChannel < ApplicationCable::Channel
end
```
Consumer(WebSocket Connectionのクライアント)は、このようにしてこれらのChannelをSubscribeできるようになる。

#### 2-2. Subscrition(Rails側での実装)
```app/channels/chat_channel.rb
class ChatChannel < ApplicationCable::Channel
  # ConsumerがこのChannelのSubscriberになると
  # このコードが呼び出される。
  def subscribed
  end
end
```

- ConsumerはChannelをSubscribeし、*Subscriber*として振る舞う。
- 生成されたメッセージは、ActionCable Consumerから送信されるIDに基づいて、これらのChannel Subscriber側にルーティングされる。

##クライアントサイド(JavacSript側)コンポーネント
### 1. Connection
#### 1-1. ConsumerとのConnection
```app/assets/javascripts/cable.js
//= require action_cable
//= require_self
//= require_tree ./channels

(function() {
  this.App || (this.App = {});

  App.cable = ActionCable.createConsumer();
}).call(this);
```

`/cable`にデフォルトで接続するConsumerが準備される。

#### 1-2. Subscriber(JavasScript側での設定)

指定のChannelにSubscriptionを作成することで、ConsumerがSubscriberになる。

```app/assets/javascripts/cable/subscriiptions/chat.coffee

App.cable.subscriptions.create{ channel: "ChatChannel", room: "Best Room" }
```
```app/assets/javascripts/cable/subscriiptions/appearance.coffee

App.cable.subscriptions.create{ channel: "AppearanceChannel" }
```

Consumerは、指定のChannelに対するSubscriberとして振る舞うことができ、回数の制限はない。
たとえば、Consumerはチャットルームを同時にいくつでもSubscribeできる。

```js
App.cable.subscriptions.create { channel: "ChatChannel", room: "1st Room" }
App.cable.subscriptions.create { channel: "ChatChannel", room: "2nd Room" }
```

## クライアント-サーバー間のやりとり
### 1. Stream
Streamは、Channelにルーティング機能を与える。
これにより、***ChannelはPublishされたコンテンツ(Broadcast)をSubscriberにルーティング***できる。

```app/channels/chat_channel.rb
class ChatChannel < ApplicationCable::Cannel
  def subscribed
    stream_for "chat_#{params[:room]}"
  end
end
```
モデルに関連するStreamがある場合、利用するBroadcastがそのモデルとChannelの両方から生成される。

```rb
class CommentsChannel < ApplicationCable::Channel
  def subscribed
    post = Post.find(params[:id])
    stream_for post
  end
end
```

これで、このChannelに次のようにBroadcastできる。

```rb
CommentsChannel.broadcast_to(@post, @comment)
```

### 2. Broadcast
- Broadcastは、Pub/Subのリンク。
- Publisherからの送信内容はすべてBroadcastを経由する。
- Broadcastは、そのPublisherのBroadcastをStreamingしているChannelの、Subscriberに直接ルーティングされる。
- 各Channelは、0個以上のBroadcastをStreamingできる。
- Streamingを行っていないConsumerは、後で接続するときにBroadcastを取得できない。


### 3. Subscription
ChannelをSubscribeしたConsumerは、Subscriberとして振る舞う。

```app/assets/javascripts/cable/subscriiptions/chat.coffee
# Web通知の送信権をサーバーからリクエスト済みであることが前提
App.cable.subscriptions.create{ channel: "ChatChannel", room: "Best Room" },
  received: (data) ->
    @appendLine(data)

  appendLine: (data) ->
    html = @createLine(data)
    $("[data-chat-room='Best Room']").append(html)

  createLine: (data) ->
    ```
    <article class="chat-line">
      <span class="speaker">#{data["sent_by"])</span>
      <span class="body">#{data["body"])</span>
    </article>
    ```

```

### 4. Channelにパラメータを渡す
Subscription作成時に、クライアント側のパラメータをサーバー側に渡すことができる。

```app/channels/chat_channel.rb
class ChatChannel < Application::Channel
  def subscribed
    stream_from "chat_#{params[:room]}"
  end
end
```
`subscriptions.create`に最初の引数として渡されるオブジェクトは、Action Cableチャンネルのparamsハッシュになります。キーワードのchannelの指定は省略できません。

```app/assets/javascripts/cable/subscriptions/chat.coffee
App.cable.subscriptions.create { channel: "ChatChannel", room: "Best Room" },
  received: (data) ->
    @appendLine(data)

  appendLine: (data) ->
    html = @createLine(data)
    $("[data-chat-room='Best Room']").append(html)

  createLine: (data) ->
    ```
    <article class="chat-line">
      <span class="speaker">#{data["sent_by"])</span>
      <span class="body">#{data["body"])</span>
    </article>
    ```
```

```rb
# アプリ内のどこかで呼び出されるコード
ActionCable.server.broadcast(
  "chat_#{room}",
  sent_by: 'Paul',
  body: 'This is a cool chat app.'
)
```

### 5. メッセージを再Broadcastする
あるクライアントから、接続している別のクライアントに、メッセージを再Broadcastすることはよくある。

```app/channels/chat_channel.rb
class ChatChannel < ApplicationCable::Channel
  def subscribed
    stream_from "chat_#{params[:room]}"
  end

  def receive(data)
    ActionCable.server.broadcast("chat_#{params[:room]}", data)
  end
end
```

```app/assets/javascripts/cable/subscriptions/chat.coffee
App.chatChannel = App.cable.subscriptions.create{ channel: "ChatChannel", room: "Best Room" },
  received: (data) ->
    # data => { sent_by: "Paul", body: "This is a cool chat app." }
App.chatChannel.send({ sent_by: "Paul", body: "This is a cool chat app." })
```
再Broadcastは、接続しているすべてのクライアントで受信される。
送信元クライアント自身も再Broadcastを受信する。
利用するparamsは、ChannelをSubscribeするときと同じ。

## フルスタック(なコンポーネント)の例

###設定手順

1. Connectionを設定
2. 親Channelを設定
3. Consumerを接続(Subscription)

### 例1
ユーザーがオンラインかどうか、ユーザーがどのページを開いているかという情報を追跡するChannelの簡単な例。

```app/channels/appearance_channel.rb
# サーバー側
class AppearanceChannel < ApplicationCable::Channel
  def subscribed
    current_user.appear
  end

  def unsubscribed
    current_user.disappear
  end

  def appear(data)
    current_user.appear(on: data['appearing_on'])
  end

  def away
    current_user.away
  end
end
```

Subscriptionが開始されると、subscribedコールバックがトリガーされ、そのユーザーがオンラインであることが示される。
このアピアランスAPIをRedisやデータベースなどと連携することもできる。

```app/assets/javascripts/cablesubscriptions/appearance.coffee
# クライアント側
App.cable.subscriptions.create "AppearanceChannel",
# Subscriptionがサーバー側で利用可能になると呼び出される。
  connected: ->
    @install()
    @appear()

# WebSocket Connectionが閉じると呼び出される。
  disconnected: ->
    @uninstall()

# Subscriptionがサーバーに拒否されると呼び出される。
  rejected: ->
    @uninstall()

  appear: ->
    # サーバーの`AppearanceChannel#appear(data)`を呼び出す
    @perform("appear", appearing_on: $("main").data("appearing-on"))

  away: ->
    # サーバーの`AppearanceChannel#away`を呼び出す
    @perform("away")

  buttonSelector = "[data-behavior~=appear_away]"

  install: ->
    $(document).on "turbolinsk:load.appearnace", =>
      @appear()

    $(document).on "click.appearnace", buttonSelector, =>
      @away()
      false

    $(buttonSelector).show()

  uninstall: ->
    $(document).off(".appearance")
    $(buttonSelector).hide()
```

1. ***クライアント***はサーバーに`App.cable = ActionCable.createConsumer("ws://cable.example.com")`経由で接続する(cable.js)。サーバーは、このConnectionの認識に`current_user`を使う。
2. ***クライアント***はアピアランスChannelに`App.cable.subscriptions.create(channel: "AppearanceChannel")`経由で接続する(appearnace.coffee)
3. ***サーバー***は、アピアランスChannel向けに新しいSubscriptionを開始したことを認識し、サーバーの`subscribed`コールバックを呼び出し、`current_user`の`appear`メソッドを呼び出す。
4. ***クライアント***は、Subscriptionが確立したことを認識し、`connected`(appearance.coffee)を呼び出す。これにより、`@install`と`@appear`が呼び出される。`@appear`はサーバーの`AppearanceChannel#appear(data)`を呼び出して`{ appearing_on: $("main").data("appearing-on") }`のデータハッシュを渡す。なお、この動作が可能なのは、クラスで宣言されている(コールバックを除く)全パブリックメソッドが、サーバー側のChannelインスタンスから自動的に公開されるからです。公開されたパブリックメソッドは、Subscriptionで`perform`メソッドを使って、RPC(リモートプロシージャコール)として利用できる。
5. ***サーバー***は、`current_user`で認識したConnectionのアピアランスChannelで、`appear`アクションへのリクエストを受信する。(appearance_channel.rb) サーバーは`:appearing_on`キーを使ってデータをデータハッシュから取り出し、`current_user.appear`に渡される`:on`キーの値として設定する。

## 例2 新たなWebノーティフィケーションを受け取る

アピアランスの例では、WebSocket Connectionを使って、サーバーからクライアントを呼び出していた。
WebSocketでは双方向通信を利用できるので、この例では、サーバーがクライアントのアクションを呼び出してみる。

このWeb Notificfation Channelは、正しいStreamにBroadcastを行ったときに、クライアント側でWeb通知を表示する。

```app/channels/web_notifications_channel.rb
# サーバーサイド
class WebNotificationsChannel < ApplicationCable::Channel
  def subscribed
    stream_for current_user
  end
end
```

```app/assets/javascripts/cable/subscriptions/web_notifications.coffee
# クライアントサイド
# サーバーからWebノーティフィケーションの送信権を
# リクエスト済みであることが前提
App.cable.subscriptions.create "WebNotificationsChannel",
  received: (data) ->
    new Notification data["title"], body: data["body"]
```

アプリケーションのどこからでも、Web Notification ChannelのインスタンスにコンテンツをBroadcastできる。

```.rb
# このコードはアプリケーションのどこか(NewCommentJobあたり)で呼び出される
WebNotificationsChannel.broadcast_to(
  current_user,
  title: 'New things!',
  body: 'All the news fit to print'
)
```
`WebNotificationsChannel.broadcast_to`呼び出しでは、ユーザーごとに異なるBroadcast名が使われるが使われている下で、現在のSubscriptionアダプタのpubsubキューにメッセージを設定する。
IDが1のユーザーなら、ブロードキャスト名は`web_notifications:1`のようになる。

このChannelは、`web_notifications:1`に着信するものすべてを、`received`コールバック呼び出しによってクライアントに直接Streamingするようになる。
引数として渡されたデータは、サーバー側のBroadcast呼び出しに2番目のパラメータとして渡されたハッシュ。このハッシュはJSONでエンコードされ、receivedとして受信したデータ引数から取り出される。


## 用語まとめ

- サーバーサイド(.rb)
    - Connection
    - Channel(コントローラ)
- クライアントサイド(.js, .coffee)
    - Consumer
    - Subscriber

- Stream(ルーティング)
    -  Broadcastを経由

## 参考リンク

- [rails/actioncable-examples](https://github.com/rails/actioncable-examples)
- [Rails 5 + ActionCableで作る！シンプルなチャットアプリ（DHH氏のデモ動画より）](https://qiita.com/jnchito/items/aec75fab42804287d71b)
- [Rails5のAction Cableの試食](https://qiita.com/sasurai_usagi3/items/75a30cc0d2d0faa9dfe8)
- [ActionCable + Reactでチャットをつくってテストをして、と一通りやってみたのでメモった。](https://qiita.com/mmmpa/items/61ff227bbc48d8c7d2d0)