Nuxt + SSR + Vuex ORMの闇

Vuex ORMの闇を紹介しようと思ったが、v0.35.0で治っていた。と思ったら別の闇に遭遇。そして、Vuex ORM graphql pluginの闇にも遭遇。

## まとめ

- Nuxt + Vuex ORM graphql pluginをそのままSSRで使うのは難しい -> 後述の解決策1 or 4
- Nuxt + Vuex ORMをSSRで使うのは可能 -> 詳細は後述

俺氏の選択は、解決策1 + シングルトンVuex ORM + Vuex ORM graphql plugin
Vuex ORM graphql pluginをシングルトンじゃないように修正するのは辛すぎる。

## Vuex ORM graphql pluginの闇

これはVuex ORMのSSR対応版(v0.35.0)に対応していない気がする。
少なくとも正しく動くのかかんたんには検証できない。

結論、Vuex ORM graphql pluginを使いたいなら、後述の解決策1を使うしかない。
パフォーマンスが問題になったら、解決策1を解決策4に昇華させるルートもある。

## Vuex ORMの別の闇

以下の問題に遭遇。v0.34.1に戻せば良いと書いてあるが、後述のようにv0.35.0の差分を使いたいので戻せない。

https://github.com/vuex-orm/vuex-orm/issues/544

上で言及されている、以下のコミットで治る。

https://github.com/cuebit/vuex-orm/commit/2ae995dba1142f1f343af9661ca8edf3797ba17d

## Vuex ORMの正しい使い方

以下に書いてあるように、SSRの場合は、 ```this.$store.$db().model('users')``` の形式で使うと良い。2019年12月に報告があり、2020年1月のv0.35.0でこの差分が入っている。

参考資料

https://github.com/vuex-orm/vuex-orm/releases/tag/v0.35.0

https://github.com/vuex-orm/vuex-orm/issues/514

https://vuex-orm.github.io/vuex-orm/guide/digging-deeper/server-side-rendering.html

今まで普通にグローバルに使ってて気付かなかった。
ぎりぎり動いていたんだろう。

インターフェースの変更で修正量が多いので、一旦は後述の解決策1などでも良いかもしれない。

どこにも言及されていないが、初期化はvuex pluginでstore初期化のたびにデータベースを作るようにしないといけないと思う。シングルトンの名残がまだ残っているから、複雑。

## Vuex ORMの闇

サーバーサイドレンダリングで、asyncData(nuxtのやつ)で、Vuex ORMをいじると、リクエスト間でVuexのstateが混じる可能性がある。

## 集めたファクト

1. Vuex ORMではコンテクスト管理にシングルトンが使われている
2. 本番のnuxtでは、runInNewContextがfalseになるので、リクエスト間でコンテクストが共有される
3. nuxt、connect(nuxtで使われているライブラリ)、vue-server-rendererでは、直列化のコードは無い
4. nuxtのasyncDataでログ出力させたら、リクエストが返るまでにVuex ORMが参照するstoreが置き換わっていることを確認

## 詳細

### 1. Vuex ORMではコンテクスト管理にシングルトンが使われている

Vuex ORMの実装の中では、storeに以下のようにアクセスできる。
同時に２つのstoreを扱えないことを示唆している。

Container.database.store

また、以下のようなVuex ORMのインターフェースから考えても、コンテクストを渡せないので、インターフェースを変えない限り、同時に2つのstoreを扱うのは難しい。

```javascript
User.create({
  data: { id: 1, name: 'John' }
})
```

### 2. 本番のnuxtでは、runInNewContextがfalseになるので、リクエスト間でコンテクストが共有される

nuxtのコードを読んだが、runInNewContextがfalseになる。

https://github.com/nuxt/nuxt.js/blob/75a29c8b1343b35bea6930fa0b7d7f66ddd586e9/packages/config/src/options.js#L404

https://ssr.vuejs.org/ja/api/#runinnewcontext

### 3. nuxt、connect(nuxtで使われているライブラリ)、vue-server-rendererでは、直列化のコードは無い

それぞれのコードを読んだが、直列化しているコードはない。

以下、上から順に呼ばれる

1. https://github.com/nuxt/nuxt.js/blob/e8aca9eb117851047e82e94948ff8b4bcb464b1a/packages/server/src/server.js
2. https://github.com/nuxt/nuxt.js/blob/e8aca9eb117851047e82e94948ff8b4bcb464b1a/packages/server/src/middleware/nuxt.js
3. https://github.com/nuxt/nuxt.js/blob/e8aca9eb117851047e82e94948ff8b4bcb464b1a/packages/vue-renderer/src/renderer.js
4. https://github.com/nuxt/nuxt.js/blob/e8aca9eb117851047e82e94948ff8b4bcb464b1a/packages/vue-renderer/src/renderers/ssr.js
5. https://github.com/vuejs/vue/blob/6390f70c2e4ec7e51cce1d2a4ba35e2d0d328205/packages/vue-server-renderer/build.dev.js

## 4. nuxtのasyncDataでhttpリクエストのところでログ出力させたら、リクエストが返るまでにVuex ORMが参照するstoreが置き換わっていることを確認

確認方法は、Vuex ORM graphql pluginでhttpリクエストのところに以下のようなコードを仕込んで確認。「同じリクエストIDである、ならば、同じstoreアドレスである」成立すれば問題ないが、

結果、成立しなかった。abコマンドなどで並行リクエストを送ると、store == store2 != store3となってしまうことがあった。

```javascript
let seq = 1
const addressToId = new WeakMap()

async asyncData({ store }) {
    const reqId = seq++
    const store2 = Project.store()
    if (!addressToId.get(store)) addressToId.set(store, seq++)
    if (!addressToId.get(store2)) addressToId.set(store2, seq++)
    console.log('request started ' + reqId + ' ' + addressToId.get(store) + ' ' + addressToId.get(store2))

    // await http request

    const store3 = Project.store()
    if (!addressToId.get(store3)) addressToId.set(store3, seq++)
    console.log('request finished ' + reqId+ ' ' + addressToId.get(store) + ' ' + addressToId.get(store3))
  },
```

## 解決策1

nuxtのモジュールで、vueのrenderの前後にフック(幸い、vueの実装ではawaitされている)を仕掛けて、直列実行させる。

並行実行されなくなるので、サーバーのスループットが犠牲になるが、nuxtなら一人のユーザーが一度に行うSSRリクエストは一回だけなので、ユーザーから見たパフォーマンスはあまり劣化しないはず。

以下のフックを使う。

https://github.com/nuxt/nuxt.js/blob/dev/packages/vue-renderer/src/renderers/ssr.js#L65-L77

nuxt module例 (moduleの使い方は https://ja.nuxtjs.org/guide/modules/ )
サーバーサイドのみなのでWeakMapを使ってみた。

```javascript

let seq = 1
let lockPromise
const auxData = new WeakMap()

const lock = async (data) => {
  while (lockPromise) {
    console.log('serial-render waiting')
    await lockPromise
  }
  lockPromise = new Promise((resolve) => {
    data.lockPromiseResolve = resolve
  })
}

const unlock = (data) => {
  if (data.lockPromiseResolve) {
    lockPromise = void 0
    const resolve = data.lockPromiseResolve
    data.lockPromiseResolve = void 0
    resolve()
  }
}

export default function () {
  this.nuxt.hook('vue-renderer:ssr:prepareContext', async ({ req }) => {
    const data = { id: seq++ }
    auxData.set(req, data)
    await lock(data)
    console.log('serial-render start ' + data.id)
  })

  this.nuxt.hook('vue-renderer:ssr:context', async ({ req }) => {
    const data = auxData.get(req)
    console.log('serial-render finished ' + data.id)
    unlock(data)
  })

  this.nuxt.hook('render:errorMiddleware', async (connectInstance) => {
    connectInstance.use((err, req, res, next) => {
      const data = auxData.get(req)
      if (data) {
        console.log('serial-render error ' + data.id)
        unlock(data)
      }
      next(err)
    })
  })
}

```

懸念: ↑のフックの間でthrowすることは無いか？ -> asyncDataとかでthrowしたらthrowした。そして、vue-renderer:ssr:contextは呼ばれなかった。

throwしたら以下に行く

https://github.com/nuxt/nuxt.js/blob/dev/packages/server/src/middleware/nuxt.js#L85

そしてnextで以下に行く。ここで独自の例外補足用のエラーmiddlewareを入れておけば良い。

https://github.com/nuxt/nuxt.js/blob/dev/packages/server/src/server.js#L165

## 解決策2

vue-server-renderのrunInNewContextに'fork'モードを追加する。

railsのunicornやpumaみたいに、ライブラリを読み込んだあとのプロセスを用意しておいて、そこからforkでサーバープロセスを作るパターンを使う。

linux上のnodeでnative moduleでforkが使えることは過去に確認したことがあるが、OSに依存するので不安定かもしれない。

## 解決策3

解決策2と似ているがZone.jsを使う。thread local storage的なものらしい。
nuxtのモジュールを作るのと、Vuex ORMの実装を変更する必要があるが、Vuex ORMのインターフェースは変更する必要がないのがメリット。いろいろ関係ないところまで影響しそうなのがデメリット。

https://github.com/angular/angular/tree/master/packages/zone.js

## 解決策4

解決策1の進化版。

vue-server-renderのrunInNewContextに'pool'モードを追加する。
contextプールを用意して空いているものに投げる。すでにtrueと'once'が実現できているから、実現できるはず。

https://github.com/vuejs/vue/blob/52719ccab8fccffbdf497b96d3731dc86f04c1ce/src/server/bundle-renderer/create-bundle-runner.js#L107
このrunnerを複数にして、直列実行にすれば良い。

メモリ消費量が問題になるかもしれないけど。

## 解決策5

かんたんで安全だけど、レイテンシーとスループット低下。in memory cacheが使えない。
以下の設定をnuxt.config.jsに追加。
参考: https://github.com/nuxt/nuxt.js/blob/21aaef3b4825e9b11aeecf7eaa8cd2c3d24fc3ce/packages/config/src/config/render.js#L4

```javascript
  render: {
    bundleRenderer: {
      shouldPrefetch: () => false,
      shouldPreload: (fileWithoutQuery, asType) => ['script', 'style'].includes(asType),
      runInNewContext: true
    },
  }
```

俺のアプリだとnuxtの処理時間(response headerに書かれるやつ)が2倍〜3倍になった。

## 解決策6

bundleRendererにpreevaluate的なオプションを用意する。
runInNewContext: trueだけど、アイドル時に事前にevaluateしておく。
runInNewContext: trueのメリットと、レイテンシーが小さいメリットはあるが、スループットが下がる。in memoryキャッシュが使えない。