PouchDBを改造してReact Nativeでも動くようにした

> 本稿は[Hacking PouchDB to Use It on React Native](https://dev.to/craftzdog/hacking-pouchdb-to-use-on-react-native-1gjh)の日本語訳

CouchDBと同期が必要なReact Nativeアプリを開発している。[PouchDB](https://pouchdb.com/)はJSで組まれたCouchDBと同期可能なデータベースライブラリで、ブラウザを主眼としているが、React Nativeでも動かすことが出来る。
そのために自分は[react-native-sqlite-2](https://github.com/craftzdog/react-native-sqlite-2) と [pouchdb-adapter-react-native-sqlite](https://github.com/craftzdog/pouchdb-adapter-react-native-sqlite)を作った。

しかしそれだけではRNで動かすには不十分だった。
例えばRNは`FileReader.readAsArrayBuffer`をまだサポートしていないため[Attachmentsの取扱いに難があった](https://github.com/stockulus/pouchdb-react-native/issues/68)。将来的に彼らが対応してくれることを願う。

そして[pouchdb-react-native](https://github.com/stockulus/pouchdb-react-native)プロジェクトの活動が最近見られないようだ。なので自分で何とかすることにして、今回動かすことに成功した。本稿ではどうやったか説明する。

## React Native上でPouchDBを動かす手順

まずは実際にどうやってPouchDBを動かすか、手順を説明したい。
自分は今回、RN用にPouchDBのコアモジュールをハックしたパッケージを２つ作成した。
[こちら](https://github.com/craftzdog/pouchdb-react-native-demo)がデモアプリ。
これを拙作アプリ[Inkdrop](https://inkdrop.app)にて採用するつもり。

### 依存モジュールのインストール

まずはPouchDBのコアパッケージをインストールする：

```bash
npm i pouchdb-adapter-http pouchdb-mapreduce
```

次に、React Native用に手を加えたPouchDBのパッケージをインストールする：

```bash
npm i @craftzdog/pouchdb-core-react-native @craftzdog/pouchdb-replication-react-native 
```

そしてSQLite3エンジンのモジュールをインストールする：

```bash
npm i pouchdb-adapter-react-native-sqlite react-native-sqlite-2
react-native link react-native-sqlite-2
```

PouchDBで必要なpolyfill用のパッケージを入れる：

```bash
npm i base-64 events
```

### Polyfillを作る

PouchDB内で使用される関数群をpolyfillするjsファイルを作る:

```js
import {decode, encode} from 'base-64'

if (!global.btoa) {
    global.btoa = encode;
}

if (!global.atob) {
    global.atob = decode;
}

// node依存のコードが実行されないようにする
process.browser = true
```

それを`index.js`の先頭で`import`してやる。

### PouchDBを初期化する

以下のような`pouchdb.js`を作る:

```js
import PouchDB from '@craftzdog/pouchdb-core-react-native'
import HttpPouch from 'pouchdb-adapter-http'
import replication from '@craftzdog/pouchdb-replication-react-native'
import mapreduce from 'pouchdb-mapreduce'

import SQLite from 'react-native-sqlite-2'
import SQLiteAdapterFactory from 'pouchdb-adapter-react-native-sqlite'

const SQLiteAdapter = SQLiteAdapterFactory(SQLite)

export default PouchDB
  .plugin(HttpPouch)
  .plugin(replication)
  .plugin(mapreduce)
  .plugin(SQLiteAdapter)
```

必要に応じて`pouchdb-find`とかのプラグインも追加する。

### PouchDBを使う

あとはいつもの通り使用する：

```js
import PouchDB from './pouchdb'

function loadDB () {
  return new PouchDB('mydb.db', { adapter: 'react-native-sqlite' })
}
```

## PouchDBにどのように手を加えたか

React Nativeで動作させるためには、PouchDBのコアモジュールから`FileReader.readAsArrayBuffer`が呼び出されるのを阻止しなければならない。
それは、全てのattachmentsを`Blob`の代わりにBase64エンコードで取り扱うという事だ。
それは[ごく少量の行への変更で実現できる](https://github.com/craftzdog/pouchdb-react-native/commit/78de4baad85c30d8dc41e0609cff894d44cc6d33)。

### `readAsArrayBuffer`が呼ばれている場所

PouchDBは各ドキュメントのMD5ダイジェストを計算しようとする。その際に`readAsArrayBuffer`が必要となる。

`pouchdb-binary-utils/lib/index-browser.js`にて:

```js
72 function readAsBinaryString(blob, callback) {
73   if (typeof FileReader === 'undefined') {
74     // fix for Firefox in a web worker
75     // https://bugzilla.mozilla.org/show_bug.cgi?id=901097
76     return callback(arrayBufferToBinaryString(
77       new FileReaderSync().readAsArrayBuffer(blob)));
78   }
79
80   var reader = new FileReader();
81   var hasBinaryString = typeof reader.readAsBinaryString === 'function';
82   reader.onloadend = function (e) {
83     var result = e.target.result || '';
84     if (hasBinaryString) {
85       return callback(result);
86     }
87     callback(arrayBufferToBinaryString(result));
88   };
89   if (hasBinaryString) {
90     reader.readAsBinaryString(blob);
91   } else {
92     reader.readAsArrayBuffer(blob);
93   }
94 }
```

この関数は以下の`pouchdb-md5/lib/index-browser.js`から呼ばれる:

```js
24 function appendBlob(buffer, blob, start, end, callback) {
25   if (start > 0 || end < blob.size) {
26     // only slice blob if we really need to
27     blob = sliceBlob(blob, start, end);
28   }
29   pouchdbBinaryUtils.readAsArrayBuffer(blob, function (arrayBuffer) {
30     buffer.append(arrayBuffer);
31     callback();
32   });
33 }
```

どうすればこれを避けられるか？

### Attachmentsの保存部

`getAttachment`メソッドの`binary`オプションを常に無効にする。
`pouchdb-core/src/adapter.js` を以下のように変更する:

```js
714     if (res.doc._attachments && res.doc._attachments[attachmentId]
715       opts.ctx = res.ctx;
716       // force it to read attachments in base64
717       opts.binary = false;
718       self._getAttachment(docId, attachmentId,
719                           res.doc._attachments[attachmentId], opts, callback);
720     } else {
```

この変更により、attachmentsは常にbase64で取得されることに注意されたい。

### Pull Replication

リモートデータベースからのレプリケーションの際、取得されたattachmentsを blobからbase64に変換してやる必要がある。
`pouchdb-replication/lib/index.js`を以下のように変更してやる:

```js
function getDocAttachmentsFromTargetOrSource(target, src, doc) {
  var doCheckForLocalAttachments = pouchdbUtils.isRemote(src) && !pouchdbUtils.isRemote(target);
  var filenames = Object.keys(doc._attachments);

  function convertBlobToBase64(attachments) {
    return Promise.all(attachments.map(function (blob) {
      if (typeof blob === 'string') {
        return blob
      } else {
        return new Promise(function (resolve, reject) {
          var reader = new FileReader();
          reader.readAsDataURL(blob);
          reader.onloadend = function() {
            const uri = reader.result;
            const pos = uri.indexOf(',')
            const base64 = uri.substr(pos + 1)
            resolve(base64)
          }
        });
      }
    }));
  }

  if (!doCheckForLocalAttachments) {
    return getDocAttachments(src, doc)
      .then(convertBlobToBase64);
  }

  return target.get(doc._id).then(function (localDoc) {
    return Promise.all(filenames.map(function (filename) {
      if (fileHasChanged(localDoc, doc, filename)) {
        return src.getAttachment(doc._id, filename);
      }

      return target.getAttachment(localDoc._id, filename);
    }))
      .then(convertBlobToBase64);
  }).catch(function (error) {
    /* istanbul ignore if */
    if (error.status !== 404) {
      throw error;
    }

    return getDocAttachments(src, doc)
      .then(convertBlobToBase64);
  });
}
```

これで動く。
`@craftzdog/pouchdb-core-react-native` と `@craftzdog/pouchdb-replication-react-native`が必要なのはそのため。
もし問題を見つけたら[ここにPRを送って欲しい](https://github.com/craftzdog/pouchdb-react-native)。