0

色々なPromiseライブラリで1つずつ順番に処理する

最初、Qでなぜかcatchでエラーが捕まえられないぞ?と書いていたけど、require(‘q’).Promiseを使うんだと教えてもらった。ありがとうございます。



色々なNodeのライブラリがそれぞれで好きなPromiseライブラリ使ってるけど、それらのライブラリ達をいっしょにアプリ内で混ぜてつなげても大丈夫なのか?ちゃんと相互にthenでチェインしたりエラー捕まえたりできるのか?
という事と、async.eachSeriesのようにURLのリストを1つずつ順番に処理完了するのを待ちながら処理していくのはPromiseでどうやって書くのかな?
というのが気になっていたのでちょっと調べた。


勉強用リポジトリ
https://github.com/shokai/promise-study

環境

node v0.10.29 + coffee-script v1.8.0
なのでまだPromiseが標準ライブラリに入ってないNode環境


試したPromiseライブラリ

このへんが有名そうだったのでREADMEやサンプルなどを読んで、試した。
でもdeferredはインタフェースがthenableじゃないっぽかったのですぐあきらめた。


結論

es6-promiseとbluebirdとQを混ぜこぜでthen/catchでチェインさせてもちゃんと動いた。

then/catchしたいだけならes6-promise使って、
async.jsでやるような高機能な並列・並行処理の制御がしたければbluebirdやQに付いている便利な関数を使えばいいと思う。


試したコード
## いろいろなPromiseライブラリを使ってみる

{Promise} = require 'es6-promise'
# {Promise} = require 'q'
# Promise = require 'bluebird'

checkOdd = (num) ->
return new Promise (resolve) ->
if typeof num isnt 'number'
throw new Error "#{num} is not number"
resolve num % 2 is 1

for i in [0,1,2,3,null,5,"かずどん",7,8]
do (i) ->
checkOdd i
.then (res) ->
if res
console.log "#{i} is odd"
else
console.log "#{i} is not odd"
.catch (err) ->
console.error err

実行結果


色々なPromiseライブラリをつなげる、1つずつ処理する

HTTP Getして、HTMLからtitleを取り出して、それをmacのsayコマンドで読む。
という3つのPromiseをそれぞれ別々のPromiseライブラリで作る。
そして3つ繋げて1セットの処理として、URLリストを順番に処理していく。
RSSを順番に見ていくクローラー的な処理を想定していて、1つ終わったら3秒待ってから次のURLを見に行くようにした。

試しに書いてみたコード
ちなみにURLリストを全部同時に処理するversionもある


下の方でBluebird.eachを使って1つずつURLを処理していく。
thenのチェインが最後まで走ったら3秒待ってから次のURLを処理するし、どこか途中でエラーが起きたらcatchして5秒待って次のURLの処理に行く。
とにかく同時に複数のHTTPリクエストは送らない。

こういうのをasync.eachSeriesで書くとけっこう頭が疲れるコードになると思うけどPromise使ったらすんなり書けた。

es6-promiseとbluebirdとQを混ぜて使っているけどちゃんと動いた。

## HTMLを(1つずつ)取得してtitleを取り出してsayで読み上げる
## いろいろなPromiseライブラリを混ぜて使ってみる
## HTTPリクエストするのは3000 msecごと
## 途中でエラーがあったら5000 msec待ってから、次のHTTPリクエストする

request = require 'request'
cheerio = require 'cheerio'
{exec} = require 'child_process'

process.env.DEBUG ||= '*'
debug = require('debug')('promise-study')


{Promise} = require 'es6-promise'
Q = require('q')
Bluebird = require 'bluebird'

urls = [
'http://shokai.org'
'そんなURLはない' # URLじゃない文字列。requestの例外を発生させるため
'https://github.com'
'https://github.com/robots.txt' # HTMLが返ってこないURL。titleタグ取得のエラーを起こすため
'https://google.co.jp'
]

# HTML本文を取得するPromise
# URLが間違っていたりすると失敗する
getHtml = (url) ->
debug "getHtml(#{url})"
return new Q.Promise (resolve, reject) -> # Qを使う
request url, (err, res, body) ->
if err or res.statusCode isnt 200
return reject(err or "statusCode: #{res.statusCode}")
resolve body

# HTMLからタイトルを取得するPromise
# HTMLじゃなければ失敗する
getTitle = (html) ->
debug "getTitle(html)"
return new Bluebird (resolve, reject) -> # Bluebirdを使う
$ = cheerio.load html
if title = $('title').text()
return resolve title
reject 'title not found'

# 音声読み上げするPromise
speech = (txt) ->
debug "speech(#{txt})"
return new Promise (resolve, reject) -> # es6-promiseを使う
exec "say #{txt}", (err, stdout, stderr) ->
return reject(txt) if err
resolve(txt)

# URLリストをBluebird.eachで1つずつ処理する
Bluebird.each urls, (url) ->
getHtml url
.then getTitle
.then speech
.then (title) ->
return new Q.Promise (resolve) -> # Qを使う
debug "wait 3000 msec"
# 3秒待ってから次のURLの処理へ
setTimeout ->
debug "wait done"
debug "!!OK #{url} - #{title}"
resolve()
, 3000
.catch (err) ->
return new Promise (resolve, reject) -> # es6-promiseを使う
debug "!!ERROR #{url} - #{err}"
debug "wait 5000 msec for Error"
# どこかでエラーあったら5秒待つ
setTimeout ->
debug "wait done"
resolve()
, 5000


実行結果

ちゃんと3秒/5秒待って次、と順番に処理できている。
requestにURLじゃない文字列を渡した時の例外も、try catch書かずにpromiseのcatchで捕捉できている。

0

Jawbone Up24の(enter|exit)_sleep_modeイベントが復活した

Up24のボタン長押しでsleepモードと通常モードを切り替えた時に送られてくるexit_sleep_modeとenter_sleep_modeイベントが復活した。

6月ごろからイベントがwebhookでpushされて来なくなってたんだけど、昨日になって復活してるのに気づいた。

俺APIからのイベントをhubotで受けとってこういう風に通知がだせる。起きたらhue電球つけるとか寝たら消すとかも簡単にできると思う。




config =
url: 'https://ore-api.herokuapp.com'
slack:
room: "#ore"

module.exports = (robot) ->

socket = require('socket.io-client').connect config.url

socket.on 'exit_sleep_mode', (event) ->
robot.send config.slack, "@#{event.screen_name} が眠りから覚めました"


socket.on 'enter_sleep_mode', (event) ->
robot.send config.slack, "@#{event.screen_name} が眠りにつきました"

0

Jawbone Up24の運動ログをHubot経由でSlackに流す

前:Web+DB Press vol.82にJawbone Up24について書いた

先月末に発売されたWeb+DB Press vol.82で、コードが長すぎてページが足りなくて入りきらなかったネタ


研究室でにわかにjawbone up24が流行り始めていて、slackの#newsというチャットルームに「起きた(5時間寝た、3回二度寝した)」とかログが流れてくる。

そこに「何歩歩いた」あるいは「活発に活動中」というログも流すようにした。


これは記事の中に書いた俺APIというJawbone APIのプロキシを使ってる。Bluetooth LEで自動同期できるup24がちょっと歩く毎にガンガンwebhookでpushしてくるのを俺APIが受信して、さらにそこからsocket.ioでhubotに再配信してくれる。
JawboneのAPIはOAuth2で認証しないと使えないんだけど、もっと細かくてどうでもいい事に気軽に使いたかったのでAPIプロキシを立てた。あと俺が起きてるか寝てるかなんて認証かける必要なく公開されてていいと思った。ので作った。


hubot-ore-api.coffee

ちょっと長いけど、こういうhubot scriptを書いておけばチャットに運動の通知が流せる。(実際使ってるやつから抜粋してきた)
歩いた時は〜〜歩歩いたってslackに流れるし、歩かずにデスクワークしている時もpush来るので「活発に活動中」とかslackに通知するようにしている。

俺APIでoauth2認証しておけば色々なところからjawboneのイベントが使えるようになるので便利だと思う。

hubot-ore-api.coffee
debug = require('debug')('hubot-ore-api')

config =
url: 'https://ore-api.herokuapp.com'
slack:
room: "#news"

module.exports = (robot) ->

socket = require('socket.io-client').connect config.url

## jawboneから動いたイベントがpushされてくる
socket.on 'move', (event) ->
debug "move - #{JSON.stringify event}"
if event.action is 'updation'
notify_move event

last_notify_at = {}
## 動いた事を通知する
notify_move = (event) ->
if Date.now() - (last_notify_at[event.screen_name] or 0) < 1000*60*60 # 1時間毎に間引く
debug "throttled #{event.screen_name}'s notify_move"
return
last_notify_at[event.screen_name] = Date.now()
get_activity "moves", event.screen_name, event.event_xid, (err, move) ->
if err or move.details?.steps < 1
debug 'no steps data in event'
return
current_steps = move.details.steps
last_steps = robot.brain.get("steps_#{event.screen_name}") or 0
robot.brain.set("steps_#{event.screen_name}", current_steps)
if last_steps > current_steps
last_steps = 0
new_steps = current_steps - last_steps
if new_steps > 0
txt = "@#{event.screen_name} が#{new_steps}歩運動しました (本日合計#{current_steps}歩 #{move.details.km}km)"
else
txt = "@#{event.screen_name} が活発に活動しています"
robot.send config.slack, txt

## ore-api.herokuappにあるJawbone APIプロキシを使う
get_activity = (type, screen_name, xid, callback = ->) ->
robot.http("#{config.url}/#{screen_name}/#{type}.json?xid=#{xid}").get() (err, res, body) ->
if err
callback err
return
try
data = JSON.parse body
catch err
callback err
return
debug data
callback null, data.data
return

0

hubot-sfc-busに湘南台19系統を追加した

綾瀬車庫〜慶応大学〜湘南台駅という路線が増えたので、追加した。


土曜13時台、本館前発は0分/20分/41分しかないが、慶応大学発(郵便局横のバス停)は13分がある。これが新しく増えた19系統

  • 綾瀬車庫発なので、本館前は通らない
  • 湘南台駅西口からは4番乗り場から出る


あと、わりと賢くオプションを解釈するようになった。

インストール方法:hubotでSFCのバス時刻表を見る

0

twitter card設定した

tweetにURLが書いてあったら中身が埋め込まれるやつを設定した。

wordpressプラグインの「Twitter Card Meta」をインストールしただけでできた。

twitterに埋め込まれるのはどうでもいいんだけど、slackでもURLから概要が展開されて表示できてよい。

設定したらhttps://cards-dev.twitter.com/validatorでチェックして、ホワイトリストに登録申請しないとtwitterでカード表示されない。