Fastifyのfastify-pluginも見てみた

今朝の続き

bufferings.hatenablog.com

ここまでの復習(自分用)

Fastifyのコアはプラグインシステム。

そのプラグインのコンテキストは親子関係を持っていて、子は親のコンテキストに登録された情報へアクセス可能だが、親は子のコンテキストに登録された情報へはアクセスできない。それによって、プラグインの影響範囲をカプセル化している。

そのコアとなるプラグインシステムの管理にはavvioを使っている。Fastifyのregisterを呼び出すとavvioのuseが呼び出されるようになっている。

親子関係の具体例

fastify.register() を呼び出して関数を渡すと、その関数がプラグインとして登録される。そのプラグイン関数が呼び出されるときにパラメータに渡されるコンテキストは、元のコンテキストの子になっている。こういう感じ。

await parentContext.register(async (childContext, opts) => {
  childContext.decorate('aaaa', 1111);

  childContext.get('/child', async (req, rep) => {
    return { hello: childContext.aaaa };
  });
});

ここでdecorateしたaaaaは、parentContextからはアクセスできない。

await parentContext.register(async (childContext, opts) => {
  childContext.decorate('aaaa', 1111);

  childContext.get('/child', async (req, rep) => {
    return { hello: childContext.aaaa };
  });
});

parentContext.get('/parent', async (req, rep) => {
  return { hello: parentContext.aaaa };
});

それぞれ叩いたらこうなる

❯ curl localhost:3000/parent
{}

❯ curl localhost:3000/child
{"hello":1111}

親のコンテキストに定義したものは子のコンテキストからもアクセス可能

await parentContext.register(async (childContext, opts) => {
  childContext.decorate('aaaa', 1111);

  childContext.get('/child', async (req, rep) => {
    return { aaaa: childContext.aaaa, bbbb: childContext.bbbb };
  });
});

parentContext.get('/parent', async (req, rep) => {
  return { aaaa: parentContext.aaaa, bbbb: parentContext.bbbb };
});

parentContext.decorate('bbbb', 2222);
❯ curl localhost:3000/parent
{"bbbb":2222}

❯ curl localhost:3000/child
{"aaaa":1111,"bbbb":2222}

fastify-plugin

このカプセル化を破って、親のコンテキストを操作したい場合はfastify-pluginを利用してラッピングしてあげる。

await parentContext.register(async (childContext, opts) => {
  console.log(`fastify-pluginを使っていない場合:${parentContext === childContext}`);
});

await parentContext.register(
  fp(async (childContext, opts) => {
    console.log(`fastify-pluginを使った場合:${parentContext === childContext}`);
  }),
);
fastify-pluginを使っていない場合:false
fastify-pluginを使った場合:true

fastify-pluginでラップすると、こんな風に、プラグインに親のコンテキスト渡されるようになる。

どうやって?

で、この記事の本題。fastify-pluginはどうやって親のコンテキストを渡してるんだろう?というのが気になったので見てみた。

ソースコードを読んでみたら、fastify-pluginが直接registerの親子関係をどうこうしているわけではなさそう。

https://github.com/fastify/fastify-plugin/blob/e868441c5d6ec2d45b91a637a6dcb8ce53a865fa/plugin.js#L45

やってるのは fn[Symbol.for('skip-override')]true にしているだけ。この skip-override はFastifyで使ってる隠しプロパティ。

なので、fastifyのコードに戻ろう

skip-overridelib/pluginUtils.js で参照されている

https://github.com/fastify/fastify/blob/1891f243ab8666ef926218691135eb032008632a/lib/pluginUtils.js#L58-L60

function shouldSkipOverride (fn) {
  return !!fn[Symbol.for('skip-override')]
}

この shouldSkipOverride はインターナルな関数で、exportされているのはこちら

https://github.com/fastify/fastify/blob/1891f243ab8666ef926218691135eb032008632a/lib/pluginUtils.js#L145-L152

function registerPlugin (fn) {
  const pluginName = registerPluginName.call(this, fn)
  checkPluginHealthiness.call(this, fn, pluginName)
  checkVersion.call(this, fn)
  checkDecorators.call(this, fn)
  checkDependencies.call(this, fn)
  return shouldSkipOverride(fn)
}

この registerPlugin を呼び出しているのが lib/pluginOverride.js

https://github.com/fastify/fastify/blob/1891f243ab8666ef926218691135eb032008632a/lib/pluginOverride.js#L28-L35

こんな感じで使っている

  const shouldSkipOverride = pluginUtils.registerPlugin.call(old, fn)

  const fnName = pluginUtils.getPluginName(fn) || pluginUtils.getFuncPreview(fn)
  if (shouldSkipOverride) {
    // after every plugin registration we will enter a new name
    old[kPluginNameChain].push(fnName)
    return old
  }

shouldSkipOverridetrue のときは、old のコンテキストにプラグイン関数の名前を登録している。

false のときは、その下で新しいコンテキストを作成してそちらにプラグイン関数の名前を登録している。

わかったきになれた!

ほんとうに読んだ通りに動いているのかは、もっと詳しく見てみないとわかんないし、登録した名前がどう使われているのかも気になるところだけど、いったんここまでで、fastifyのPluginの概要と、fastify-pluginの動きの雰囲気が分かったのでよしとしようかな。

面白かった!