Backbone.jsにcron機能を実装する30行のCoffeeScript

まあコンパイルしてJavaScriptに展開すると100行超えるんですが。

もうちょい徹底すれば25行くらいにはなりそうだけど、可読性を失う自己満足になるのでこの辺で自重。

```coffeescript:Backbone.Cronクラス
class Backbone.Cron
  constructor: (@owner, crontabs, autostart = true) ->
    seqArray = (from, to) -> _.map(new Array(to - from + 1).join().split(','), (v, i, a) -> i + from)
    parseCrontab = (crontab) =>
      ctArray = _.map(crontab.split(/\s+/), (v, i, a) ->
        return v if i is 6 or v is '*'
        v = v.replace /(\d+)-(\d+)/g, (vm, vf, vt) -> seqArray(Number(vf), Number(vt)).join()
        v = v.replace /\*\/(\d+)/g, (vm, vi) -> Number(vi) * -1
        _.map(v.split(','), (vv, vi, va) -> Number vv).sort())
      unit: @_makeunit ctArray
      exec: ctArray[6]

    @items = {}
    @items[label] = _.extend parseCrontab(ct), { crontab: ct, pause: false } for label, ct of crontabs
    @owner.bind 'remove', () => @stop() unless @owner.model?
    @start() if autostart

  start: () =>
    @unixtime = parseInt(new Date() / 1000, 10)
    @timer = setInterval () =>
      d = new Date(++@unixtime * 1000)
      now = @_makeunit [ d.getSeconds(), d.getMinutes(), d.getHours(), d.getDate(), d.getMonth() + 1, d.getDay() ]
      for label, item of @items when not item.pause
        @owner[item.exec] label, d if _.every item.unit, (v, k ,o) -> v? and (v is '*' or _.some(v, (vv, vi, va) -> if vv < 0 then now[k] % (vv * -1) is 0 else vv is now[k]))
    , 1000

  stop: () => clearInterval @timer
  on: (label) => @items[label].pause = false
  off: (label) => @items[label].pause = true
  _makeunit: (array) => _.object [ 'second', 'minute', 'hour', 'day', 'month', 'weekday' ], array
```

* デモ付きのソースを[GitHub](https://github.com/ktty1220/backbone.cron)にも上げてあります。
* デモは[こちら](http://ktty1220.github.io/backbone.cron/demo.html)

## 使い方

まず上記ソースをコピペしてJavaScriptにコンパイルしたファイルをbackbone.jsの後にロードしてください。

```html:例
<script type="text/javascript" src="/path/to/backbone.js"></script>
<script type="text/javascript" src="<上記ソースをコピペしてjsにコンパイルしたファイル>"></script>
```

`Backbone.～.extend`の`initialize()`でBackbone.Cronオブジェクトを作成します。

```js:モデルにcronを仕込む例
var HogeModel = Backbone.Model.extend({
  initialize: function () {
    this.cron = new Backbone.Cron(this, {
      test1: '0,45,20-23 * * * * * test1',
      test2: '39 */6,10,20,40,50 * * * * test2',
      test3: '0 */5 */2 * * * test2'
    });
  },

  test1: function (label, now) {
    console.log(label, now);
  },

  test2: function (label, now) {
    console.log(label, now);
  }
});
```

## メソッド

### new Backbone.Cron(&lt;関連付けるオブジェクト&gt;, &lt;crontab設定&gt;[, 自動スタート設定]);

##### 引数1: 関連付けるオブジェクト

引数2のcrontabで指定したメソッドを実行させるオブジェクトです。大抵の場合は`this`で良いと思います(上記の例では`this` = HogeModelオブジェクト)。

##### 引数2: crontab設定

`ラベル`:`crontab`のペアからなる連想配列を指定します。crontabは

`秒` `分` `時` `日` `月` `曜日` `実行するメソッド`

の順に半角スペース区切りで指定します。一通りのcrontab書式が指定可能です。

ここで指定したメソッドが呼び出される時には(`ラベル`、`現在日時のDateオブジェクト`)の2つが引数で渡されます。

##### 引数3: 自動スタート設定

デフォルトではBackbone.Cronをnewした時点でタイマーが作動します。とりあえずnewはするけど実行はちょっと後でという場合には`false`を指定してください。そして必要になったら`start()`メソッドでタイマーを作動させてください。

### start()

作成したBackbone.Cronオブジェクトのタイマーを作動させます。

### stop()

Backbone.Cronオブジェクトのタイマーを停止させます。crontabで登録した全ての処理がストップされます。

### off(&lt;ラベル&gt;)

指定したラベルの処理だけ実行させないようにします。

### on(&lt;ラベル&gt;)

`off()`で止めたラベルの処理を再度実行させるようにします。

## 注意点

Backbone.Cronを仕込んだオブジェクトを削除する前には必ず`stop()`メソッドでタイマーを止めてください。そうしないとオブジェクトを削除した後にもタイマー処理が動き続け、メモリリークが発生します。

なお、Backbone.Cronの内部では、`remove`イベントで`stop()`が発生するようにbindしているので、Backbone.Modelに関連付けられたBackbone.Cronオブジェクトに関しては、Model削除時に自動的にタイマーが停止されます。

ただし、`Collection.reset()`の場合、(Backbone.jsバージョン1.0.0の段階では)Modelに対して`remove`イベントが発生しないのでタイマーが停止されません。そこで、Backbone.Collection.extendで`reset()`の処理を以下の様に拡張しておけば、`reset()`時に`remove`イベントが発火されるようになります。

```js:Backbone.Collectionのreset()を拡張
var HogeCollection = Backbone.Collection.extend({
  model: HogeModel,

  reset: function (models, options) {
    this.each(function (model) {
      model.trigger('remove');
    });
    return HogeCollection.__super__.reset.apply(this, arguments);
  },
```

## 最後に

Backbone.jsに特化しているのは`remove`イベントのbindだけなので、この部分を潰せばBackbone.jsに限らず、色々なオブジェクトにCron処理を実装する事ができます(underscore.js依存ですが)。

ちなみにモダンブラウザとIE7以上で動作確認済みです。