ServiceWorkerのBackground Syncでオンライン復帰時にデータ送信

#はじめに
2016年3月にリリースされたChrome 49から[Background Sync](https://wicg.github.io/BackgroundSync/spec/)という新しい機能を使えるようになった。この機能を使うと、オフライン時に送信しようとしたデータを、その後ブラウザが閉じられたとしても、オンラインに復帰した際に自動的に送信することができる。そこで、[LocChat.com](https://locchat.com)というチャットサイトでこの機能を試してみた。
[![Background Sync demo](https://media.giphy.com/media/xT1XGCOcuMhFXbwpr2/giphy.gif)](https://youtu.be/RHDfqAr091E)
左のAndroidの画面で、機内モードに設定後、送信ボタンを押し、ブラウザを終了後、機内モードをオフにすると、右のPCのブラウザにチャットが届いている。

#Background Syncの使い方
##ServiceWorkerの登録
まず、Background SyncはServiceWorkerを使うので、下記のコードでServiceWorkerを登録する。

```js
navigator.serviceWorker.register('./sw.js', {scope: './'})
```

##送信処理
ユーザーが送信ボタンを押した際には下記のようにして、ブラウザがBackground Syncに対応している場合は`this._sendMassageWithSync(data)`を実行し、それに失敗したり、ブラウザが対応していない場合は、`this._sendMessage(data)`を実行している。

```js
if (navigator.serviceWorker && window.SyncManager) {
  this._sendMassageWithSync(data)
    .catch((function() {
      this._sendMessage(data);
    }).bind(this));
} else {
  this._sendMessage(data);
}
```
##Syncの登録
`this._sendMassageWithSync(data)`では下記のようにIndexedDBの中にdataを保存してから、`reg.sync.register()`を呼び出しSyncを登録している。引数として渡すタグ名に使っている`result`は、IndexedDBで自動採番された`id`が渡ってくる。

```js
  _sendMassageWithSync: function(data) {
    var INDEXED_DB_NAME = 'send_queue';
    var INDEXED_DB_VERSION = 1;
    var STORE_NAME = 'messages';
    function openDataBase() {
      return new Promise(function(resolve, reject) {
          var req = indexedDB.open(INDEXED_DB_NAME, INDEXED_DB_VERSION);
          req.onupgradeneeded = function (event) {
            req.result.createObjectStore(
                STORE_NAME, { keyPath: 'id' , autoIncrement: true });
          };
          req.onsuccess = function (event) { resolve(req.result); }
          req.onerror = reject;
        });
    }
    function addMessageToDataBase(data) {
      return openDataBase().then(function(db) {
          return new Promise(function(resolve, reject) {
            var transaction = db.transaction(STORE_NAME, 'readwrite');
            var store = transaction.objectStore(STORE_NAME);
            var req = store.add(data);
            req.onsuccess = function() { resolve(req.result); }
            req.onerror = reject;
          });
        });
    }
    return addMessageToDataBase(data).then(function(result) {
        return navigator.serviceWorker.ready.then(function(reg) {
            return reg.sync.register('send-msg:' + result);
          });
      });
  },
```

`this._sendMessage(data)`では単純にXMLHttpRequestで送信しているだけなので省略。

##ServiceWorkerのSyncEventハンドラ
オンライン時に呼ばれるSyncEventハンドラではSyncのタグ名から先程の`id`を取り出し、`sendAndDeleteMessage(id)`を実行して返ってくるPromiseを`evt.waitUntil()`に渡している。こうすることで、`sendAndDeleteMessage`が失敗した場合、つまりネットワークが再度切断された場合に、再度オンライン復帰時にSyncEventハンドラが呼ばれるようになる。

```js
self.addEventListener('sync', function(evt) {
  if (evt.tag.startsWith('send-msg:')) {
    var id = parseInt(evt.tag.substr(9))
    if (isNaN(id))
      return;
    evt.waitUntil(sendAndDeleteMessage(id));
  }
});
```

`sendAndDeleteMessage()`では`getMessage(id)`でIndexedDBからデータを取ってきて、`sendRequest`で実際に送信し、`deleteMessage()`でIndexedDBからデータを削除している。

```js
function sendAndDeleteMessage(id) {
  return getMessage(id).then(sendRequest).then(function() {
    return deleteMessage(id);
  });
}
```

##IndexedDBからデータの取得
`getMessage(id)`では`openDataBase()`で取得した`IDBDatabase`から`IDBObjectStore`を取得し、`store.get(id)`でデータを取得している。

```js
function getMessage(id) {
  return openDataBase().then(function(db) {
      return new Promise(function(resolve, reject) {
        var transaction = db.transaction(STORE_NAME, 'readonly');
        var store = transaction.objectStore(STORE_NAME);
        var req = store.get(id);
        req.onsuccess = function() { resolve(req.result); }
        req.onerror = reject;
      });
    });
}
```

##データの送信
`sendRequest`ではFetch APIを使ってデータを実際に送信している。

```js
function sendRequest(request) {
  if (!request)
    return Promise.resolve();
  var formData = new FormData();
  formData.append('pid', request.pid);
  formData.append('content', request.content);
  return fetch(new Request(
      '/m',
      {method: 'POST', body: formData, credentials: 'include'}));
}

```


##IndexedDBからデータの削除
`deleteMessage()`では、不要になったデータをIndexedDBから削除している。

```js
function deleteMessage(id) {
  return openDataBase().then(function(db) {
      return new Promise(function(resolve, reject) {
        var transaction = db.transaction(STORE_NAME, 'readwrite');
        var store = transaction.objectStore(STORE_NAME);
        var req = store.delete(id);
        req.onsuccess = resolve;
        req.onerror = reject;
      });
    });
}
```

#参考
https://developers.google.com/web/updates/2015/12/background-sync
https://wicg.github.io/BackgroundSync/spec/