Sails.js(0.10.x)でチャットアプリを作りながらPub/Subで柔軟に通知するテクニックを学ぶ

# Sails.jsでチャットアプリを作りながらPub/Subで柔軟に通知するテクニックを学ぶ

## はじめに
まずは前回の記事を読んでいただけるとスムーズに理解出来るかと思います。

[Sails.js(0.10.x)でSocket.IOを使った超シンプルなチャットサービスを作る](http://qiita.com/KeitaMoromizato/items/fce04753d3ea253fa8a1)

また、今回はPub/Subに関するテクニックですが、その肝心のテクニックはだいぶ後ろのほうにかかれています。この記事を元に初心者が簡単なチャットサービスを作れるところを目指していますので、有識者には退屈かと思います。適当に読み飛ばして下さい。

## 準備
基本的に[前回の記事](http://qiita.com/KeitaMoromizato/items/fce04753d3ea253fa8a1)で作成したアプリケーションに修正を加えていきますので、まずはそちらを参考にしてください。

今回使ったアプリは以下のレポジトリに置いています。参考までに。
https://github.com/KeitaMoromizato/chatsample2

## Controllerのメソッドを作成
前回`sails generate api message`というコマンドを実行しているので、`/api/controllers/MessageController.js`が生成されているはずです。まずはここにメソッドを追加し、Controllerの実装方法を学びましょう。

必要なのは`GET /message`と`POST /message`の処理なので、以下のように`find()`と`create()`の２つのメソッドを書きます。

``` js:api/controllers/MessageController.js
module.exports = {
	find: function(req, res) {
		console.log("GET /message");

		Message.find().exec(function(err, messages) {
			res.json(messages);
		});
	},

	create: function(req, res){
		console.log("POST /message");

		var text = req.param('text');

		Message.create({text: text}).exec(function(err, message) {
			res.json(message);
		});
	}
};
```

これだけで動いてしまいます。ブラウザから見た動作は前回と変わりませんが、Sails.jsを起動したコンソールを見てみるとしっかりとログが出力されているはずです。

Sails.jsには`Blueprint`という機能が備わっており、`GET /message`のリクエスト時には`find()`が、`POST /message`のリクエスト時には`create()`が自動的に実行されるようになっています。とりあえず動かしてみたいときには非常に楽です。

他にもあるので詳細は公式リファレンスを。
http://sailsjs.org/#/documentation/reference/blueprint-api

## Pub/Subを実装する
前回ブラウザを２つ立ち上げてリアルタイム通信の動作確認までしましたが、上記実装をしてしまうとそれが動かなくなっています。メソッドを何も書いていない場合は`socket.get()`のリクエストが来た場合は自動的にSubscribe(予約)が実行され、`socket.post()`のリクエストでPublish(通知)が実行されます。

なので自分でPublish/Subscribeする処理を追記しましょう。

``` js:api/controllers/MessageController.js
module.exports = {
	find: function(req, res) {
		console.log("GET /message");

		Message.find().exec(function(err, messages) {

			Message.subscribe(req.socket);
			res.json(messages);
		});
	},

	create: function(req, res){
		console.log("POST /message");

		var text = req.param('text');

		Message.create({text: text}).exec(function(err, message) {

			Message.publishCreate(message);
			res.json(message);
		});
	}
};
```

追加された行を見て行きましょう。まずはfind()内での以下の行。これは「req.socketに対してMessageに関する通知があったら送ります」という命令。フロントとでは`socket.get()`でMessageモデルを取得しているので、以降Messageモデルに関する通知を受け取ることが出来ます。

```js
Message.subscribe(req.socket);
```

次は`create()`の中の一文。内容はメソッド名そのままで、「Messageモデルが作成されたので通知しますよ」ということ。これでPub/Subの実装は完了！

```js
Message.publishCreate(message);
```

## 複数のスレッドに分割する
前置きが長くなってしまいましがここからが本題。前回は最速で作れるチャットサービスを目指してやっていましたが、正直こんなサービス使い物にならないですよね。なのでもう少し実用性を高めるため、トピックごとの会話が出来る「スレッド」という概念を入れます。SlackでいうところのChannelです。これを擬似的に創りましょう。

まずはController/Modelを作るところから。

```
$ sails generate api thread
```

ThreadControllerを実装します。今回は簡略化のため、POSTされたスレッドと同じタイトルのスレッドがすでに存在したらそれを返し、ない場合は新規作成しています。

```js:api/controller/ThradController.js
module.exports = {
  create: function(req, res) {
    console.log("POST /thread");
    var title = req.param('title');

    Thread.findOne({title: title}).exec(function(err, thread){
      // すでに同名のスレッドがあれば返す
      if (thread) {
        console.log("thread already exists");
        return res.json(thread);
      }

      Thread.create({title: title}).exec(function(err, thread) {
        res.json(thread);
      });
    });
  }
};
```

Viewはこんな感じ。デザインセンスが無いわけではなくデザインを放棄しただけです。

![23.png](https://qiita-image-store.s3.amazonaws.com/0/52054/44a60a5c-2464-343e-8105-6550e8c7faa6.png)

```js:assets/js/main.js
(function() {

  // Socket.IOに接続
  var socket = window.io.connect();
  var currentThread;

  socket.on('connect', function() {
    // 以下の処理はSocket.IOのconnectメッセージ受信後(接続確立後)
    // に行わないと失敗する

    socket.on('message', function(message) {
      if (message.verb == "created") {
        $("#chat-timeline").append('<li>' + message.data.text + '</li>');
      }
    });
  });

  $('#chat-send-button').on('click', function() {
    if (!currentThread) return;

    var $text = $('#chat-textarea');

    var msg = $text.val();

    // messageをthreadに紐付けるためThreadIDを追加
    socket.post("/message", {
      text: msg,
      thread_id: currentThread.id
    }, function(res) {
      $text.val('');
    });
  });

  $('#thread-create-button').on('click', function() {
    var $text = $('#thread-form');

    var title = $text.val();

    // Thread作成or取得
    socket.post("/thread", {
      title: title
    }, function(res, a) {
      $('#current-thread').text("in " + res.title);
      // 取得したThreadのメッセージを取ってくる
      getMessages(res.id);
      currentThread = res;
      $text.val('');
    });
  });

  var getMessages = function(threadId) {
    socket.get("/message", {thread_id: threadId}, function(messages) {
      for (var i = 0; i < messages.length; i++) {
        $("#chat-timeline").append('<li>' + messages[i].text + '</li>');
      }
    });
  }
})();
```

実行してみると、しっかりとThreadごとにメッセージが保存されていることが分かります。また、2画面で開いてもリアルタイム通知がもう一方の画面にも届いていることが分かります。

![24.png](https://qiita-image-store.s3.amazonaws.com/0/52054/c7026531-6b03-ab82-aa20-4fec82dee246.png)

がしかし、これ実はうまくいってません。２つのウィンドウで__別々のThreadを開いていても、通知されてしまいます。__普通のチャットアプリで考えると、同じスレッドを開いている人にだけ通知してほしいものです。

ここまで来てやっとこの記事の目的、__いい感じにpublishするにはどうしたらいいのか？__というところです。

## Publishを振り分ける
まず上の問題点はなんだったのか？それは__Messageモデルを取得した人に対し、全てのMessageの通知を投げている__ところ。そこにThreadの概念はなく、どれだけ人が増えようとも、Threadが増えようとも全員に通知してしまいます。

これを解決するにはどうするか？方法は結構シンプルで、__Threadに対してpublishを投げればいい__ということ。では実際に見て行きましょう。

まずはControllerから。ポイントは１箇所だけ。`        Thread.subscribe(req.socket, thread);`の部分。Thradを読み込んだ(作成した)ユーザに対して通知をするために、subscribe()しています。第２引数を設定した場合、指定したモデルにのみpublishを送るようになります(というか第２引数指定しない場合deprectedって怒られてますね。動きはするけど...)

``` api/controller/ThradController.js
module.exports = {
  create: function(req, res) {
    console.log("POST /thread");
    var title = req.param('title');

    Thread.findOne({title: title}).exec(function(err, thread){
      // すでに同名のスレッドがあれば返す
      if (thread) {
        console.log("thread already exists");
        Thread.subscribe(req.socket, thread);
        return res.json(thread);
      }

      Thread.create({title: title}).exec(function(err, thread) {
        Thread.subscribe(req.socket, thread);
	res.json(thread);
      });
    });
  }
};
```
一方MessageControllerの方は、Messageモデルに対する通知をやめたので、`find()`からsubscribeを消しました。そして、一番重要な点が`create()`にて__Messageモデルが作成された時にThreadに対してpublishをしている__ところです。ここが今日のポイント！

また、先ほどはpublichCreate()を使っていましたが、今回はpublishUpdate()です。Threadモデル自体はもう存在するので、そのモデルに対する擬似的な変更通知としてMessageを投げます。

``` api/controller/MessageController.js
module.exports = {
  find: function(req, res) {
    console.log("GET /message");

    var threadId = req.param('thread_id');
    console.log(threadId);

    Message.find({thread: threadId}).exec(function(err, messages) {
      res.json(messages);
    });
  },

  create: function(req, res){
    console.log("POST /message");
    var text = req.param('text');
    var threadId = req.param('thread_id');

    Message.create({thread: threadId, text: text}).exec(function(err, message) {
      Thread.publishUpdate(message.thread, {
        model: 'message',
        body: message
      });
      res.json(message);
    });
  }
};
```

あとはクライアント側で受け取るだけ。socketでthreadモデルに対して待ち受け、update通知ならデータを取り出し表示。これで完了！

```assets/js/main.js
socket.on('thread', function(thread) {
  if (thread.verb == "updated" && thread.data.model == 'message') {

    $("#chat-timeline").append('<li>' + thread.data.body.text + '</li>');
  }
});
```

## 動作確認
ここまで実装すると、こんな感じで(といっても静止画だとよくわからないので動かしてみて欲しいのですが)同じスレッドを開いている人に対してのみ通知が行くようになります。やったね！

![25.png](https://qiita-image-store.s3.amazonaws.com/0/52054/6c59d9bc-1564-8840-9d51-1890581b1409.png)