🧊

miniflare@next のコードを読む

現時点ではまだリリースされてなくてベータの状態。

現行のv1について読んでみたのはこちら。

miniflare のコードを読む - console.lealog();

ちなみに、Cloudflare系スタックの開発に使うCLIの`warngler`が、なんとv2になると同時にMiniflareを内蔵したCLIになった。そしてその`wrangler`もv2はまだリリースされてない。つまりそういうことやな・・!

Overview

v2になったとはいえ、正統進化といった感じなので、機能に大きな差異があるわけではない。

読み始めたときの最新のコミットハッシュは`97e5fa1570d8a2d5032130590cd661c32244a1b1`で、公開バージョンでいうと`2.0.0.-rc.3`だった。

https://github.com/cloudflare/miniflare/tree/v2

コードベースがモノレポになって、より洗練され、より新しいNode.jsのWeb標準系のコードを使うようになったって感じ。

新機能もちょいちょい増えてて、JestのEnvironmentとしても使えるようになったりもしてる。

npm i -D jest jest-environment-miniflare@next

というわけで、コードをざっくり読み直す。

パッケージの全容はこちら。イメージしやすいよう分類別に並べ替えしておいた。

# 表向きのAPI
├── miniflare
├── cli-parser
├── http-server
├── scheduler

# そのコア
├── core
├── watcher
├── storage-file
├── storage-memory
├── storage-redis
├── runner-vm

# Workerのランタイム
├── cache
├── durable-objects
├── kv
├── sites
├── html-rewriter
├── web-sockets

# その他
├── shared
├── shared-test
└── jest-environment-miniflare

というわけで、ざっくり上から眺めていく。

エントリーポイント

`packages/miniflare`がすべてのエントリーであり、npmに公開されてるやつ。

CLIとしてキックする場合は、

  • miniflare/bootstrap
    • miniflare/src/cli
      • miniflare/src/index

モジュールとして利用する場合は、

  • miniflare/src/index

からのスタートになる。

miniflare: cli

引数をさばくところは、`packages/cli-parser`に分離されてる。

気になった引数は、`--mount`で、これは複数のWorkerを同時に立てて開発するためのもの。複数のWorkerから単一のKVを参照したりするために、あれこれするの大変そう。

miniflare: api

  • `class Miniflare extends MiniflareCore {}`
    • プラグインと、コードを実行する`VMScriptRunner`でコアを初期化
      • `packages/runner-vm`
  • `startServer()`と`startScheduler()`が、CLIでDevサーバーを立てるもの
    • コードは`packages/http-server`と`packages/scheduler`にある
  • APIとして使う場合は、コア側の`dispatchFetch()`と`dispatchScheduled()`を直接使うことになる
http-server: index
  • リクエストを受けて、コアの`dispatchFetch()`を呼ぶまでのコード
  • HTTP(s)のサーバーだけでなく、WebSocketにも対応するのでWSのサーバーも立ててる
  • Workerが`text/html`を返すときのために、LiveReload用のコードもある
    • それもWebSocketでやってる
  • Nodeで受けたリクエストを、Workerの`Request`に変換してる
    • CFWなので、`cf`プロパティなども付け足す
  • レスポンスを返すとき、gzipなどの圧縮もオンザフライでやってる
scheduler: index

XxxPlugin

`http-server`に限らず、だいたいの実装は、

  • `:pkg/src/index`: 実装それ自体
  • `:pkg/src/plugin`: プラグインとしての設定値の保持、セットアップや事前の処理

という2本柱になってて、C/C++とかでいうヘッダファイルみたいやなって思った。

  • `setup()`でグローバルオブジェクトとして使われる実装を返せる

core: index

  • `class MiniflareCore extends TypedEventTarget {}`
    • `EventEmitter`ではなく、`EventTarget`を使ってのイベント駆動
    • そして型付にするための独自実装
      • コードは`packages/shared`にある
  • `#init()`: Workerの初期化(前編)
  • `#reload()`: Workerの初期化(後編)
    • 初回はスキップされる処理もあるが、実態はこっち
    • `#init()`で用意したものを使って、コードを実行するグローバルスコープの用意など
    • そして`scriptRunner.run(globalScope, script)`
      • `addEventListener()`のWorkerの場合、その内容で待ち受け
      • ESMのWorkerの場合は、ここで`fetch`と`scheduled`が返るので、それを待ち受け
    • コードの変更を検知するWatcherも再構成
      • `packages/watcher`
  • `dispatchFetch()`
    • 自身が抱えるグローバルスコープの実装の`kDispatchFetch()`を呼ぶ
    • ユーザーが書いた`addEventListener()`は、既にこのグローバルスコープで保持されてる
    • グローバルスコープも`EventTarget`を継承してる
  • `dispatchScheduled()`
    • 同上
  • このあたりのイベントに関するコードは、`standards/event`にある
    • グローバルスコープである`ServiceWorkerGlobalScope`もここ

core: standards

  • グローバルスコープに生えてる標準APIたちの実装
    • `ServiceWorkerGlobalScope`自体
    • `addEventListener()`
    • `fetch()` / `FetchEvent`
    • `Request` / `Response`
    • `DOMException`
    • `atob()` / `btoa()`
    • etc..
  • これらがそのまま生えるわけではない
    • 生えてるものもあるけど
    • それを決めるのは実装を利用するプラグインの仕事
  • `fetch()`は、`undici`を使うようになった
    • しかし実態としてはラップしたやつを使うことになる

core: plugin

  • `MiniflareCore`に渡るプラグインリストの一員たち
  • CorePlugin
    • Miniflareの振る舞いを決める
    • CLI引数のマッピングもココにある
    • グローバルスコープで公開されるグローバルオブジェクトを決めてるのもココ
    • `global.MINIFLARE: true`
  • BuildPlugin
  • BindingsPlugin
    • 環境変数やシークレット、WASMモジュールの対応

watcher

  • スクリプトのコード変更を検知する仕組み
  • `chokidar`とか使わずに自作してる

storage-xxx

  • KVSの実装で、データをどこに保存するかで3種類ある
  • `MemoryStorage`
    • オンメモリで保存
  • `RedisStorage`
    • `ioredis`とつなげてそこに保存
  • `FileStorage`
    • ローカルファイルに保存
  • KVなどの実装のバックエンドとしてだけでなく、内部的なプラグインの設定を保持する用途にも使われる

runner-vm

  • Nodeの`vm`のラッパー
  • 実装したグローバルスコープで、ユーザーのWorkerスクリプトを実行する
  • ESMのWorkerの場合に必要なモジュールリンカもココ

その他のグローバルオブジェクト

  • Cache
    • `XxxStorage`を操作する層として実装してある
  • KV
    • `XxxStorage`を操作する層として実装してある
    • `ArrayBuffer`や`Stream`で取り出すこともできるところまで
    • Nodeの`stream/consumers|web`とかまで使い倒してる
  • DO
    • MultiRead / SingleWriteな性質を再現するMutexまで自作して再実装してある
  • Sites
    • `__STATIC_CONTENT`というKVを用意して再現
  • HTMLRewriter
    • `html-rewriter-wasm`を使ってる
  • WebSocket
    • `WebScocket`や`WebSocketPair`を実装してる
    • WSのアップグレードに対応した`fetch()`を上書き公開してる

shared

  • `compat.ts`
  • `event.ts`
    • `EventTarget`を拡張した`TypedEventTarget`がココにある
    • `ServiceWorkerGlobalScope`だけが継承してる`ThrowingEventTarget`もココ
      • リスナの実行が`try/catch`されてて、catch時に`stopImmediatePropagation()`する
  • 型まわり
    • `runner.ts` / `storage.ts` / `plugin.ts` / `wrangler.ts`
  • `sync/mutex.ts`
    • いわゆる`Mutex`の実装
    • コアがスクリプトの変更検知で実行するコールバックの同期実行を保証するのに使ってる
    • DOの部分では、`ReadWriteMutex`というのが別で実装されてる
  • `sync/gate.ts`
    • `InputGate` / `OutputGate`
    • `AsyncLocalStorage`を使って非同期処理を直列にしてるっぽい・・?
    • `WebSocket`は、ココにある`InputGatedEventTarget`を継承してる

感想

現行のv1系を読んでても思ったけど、ほんとこれ一人で書いてんのマジっすか・・って感じ。

もちろんリファクタする余地とかはあるけど、それでもこの設計とその構想を一人で練り上げてコードに落とし込むって、簡単にできることじゃない・・ほんとすごいわこの人・・。

一通りのコードを読んでみたものの、バグ対応とかOSS的なことができるかと聞かれると、それはまた別の話って感じ。

個人プロジェクトからCloudflareのプロジェクトになったけど、そのへんがこれからどうなっていくのか次第かなーと思ってる。(少なくとも以前よりIssueの反応は遅くなってるので、インターン忙しいんかなー、`wrangler2`のほうに駆り出されてるんかなーとか邪推してる)