MoonBit の Worker 用アダプターをライブラリにする #
前の記事の受け渡しは、このブログのためだけのものだった。
wasm に届くのは BASE_URL、メソッド、クエリー、パスだけで、POST の本文もヘッダーも届かない。
これを、ほかの Worker でも使える形に切り出せるかを試した。
Go の net/http に寄せることは目指さず、同じようなことができればよい、とした。
形 #
3 つに分かれた。
| 部分 | 中身 |
|---|---|
moonbit/http | Request、Response、Headers、クエリーとフォームの解析、Router。ホストに依存しないので native でもテストできる |
moonbit/http/wasm | 線形メモリを通した受け渡し。handle(n, handler) |
moonbit/http/wasm/adapter.mjs | JS の側。wasmApp(module) が fetch を返す(約 90 行) |
アプリが書くのは、ルートと、export する 1 関数だけになる。
let app : @http.Router = {
let r = @http.Router::new()
r.handle("GET /hello/{name}", req => {
let greeting = req.query().get("greeting").unwrap_or("hello")
@http.Response::text(200, "\{greeting}, \{req.param("name")}\n")
})
r.handle("POST /form", req => ...req.form()...)
r
}
pub fn serve(n : Int) -> Int {
@wasm.handle(n, req => app.serve(req))
}import wasm from "../../../../_build/wasm-gc/release/build/pkg/http/wasm/integration/integration.wasm";
import { wasmApp } from "../adapter.mjs";
export default wasmApp(wasm);要求は、メソッド、URL、ヘッダー、Worker の変数、本文の順に、線形メモリに書く。 本文の前までは改行で区切った UTF-8 のテキストで、本文はバイト列のまま置く。 だからバイナリのアップロードも、バイナリの応答も、そのまま通る。
ルーター #
Go 1.22 の ServeMux のパターンをまねた。
GET /posts/{slug}、/files/{path...}、/{$}のように書く。GET のパターンは HEAD にも当たる。- パスは合うがメソッドが違うときは 405 と
Allow、どれにも合わないときは 404。 - より狭いパターンが勝つ。Go はあいまいな重なりを登録すると panic するが、こちらは登録の順で決める。
{slug}.mdのような、wildcard の後ろに続く接尾辞は書けない。
試しに、このブログのルーティングもこのルーターに載せ替えた。
1 つだけ食い違った。Go 版のブログはデコードした後のパスで手で分岐しているので、/docs%2F は /docs/ になる。
ServeMux と同じく、セグメントに分けてからデコードすると docs/ という 1 つのセグメントになり、404 になる。
デコードした後のパスで分けるオプション(decoded_path)を足して合わせた。
確かめたこと #
例として、moonbit/pkg/http/wasm/integration に、POST、クエリー、テキストと JSON の本文、urlencoded と multipart のフォーム(100 KB のバイナリのファイル)、PUT と DELETE、405、複数の Set-Cookie、バイナリの応答、Worker の変数を使うルートを置いた。
Node と wrangler dev の両方で動いた。
このブログは、ライブラリに載せ替えた後も、Go 版と比べて前と同じパスが一致している。 wasm は約 5 KB 大きくなり、速さは変わらなかった。
1 つ、汎用にしたことで増えた危険があった。 前の Worker は POST の本文を読まずに 404 を返していた。 新しいアダプターは本文を wasm のメモリに写すが、wasm のメモリは一度増えると減らない。 大きな POST を送られると、isolate のメモリを食いつぶしうる。 そこで、本文の上限(既定は 1 MiB、超えたら 413)を足し、このブログは本文を wasm に渡さないようにした。
まだできないこと #
- **非同期の I/O。**
serveは同期なので、ハンドラーから KV や R2、外へのfetchを呼べない。ライブラリとして使うなら、ここがいちばん大きな壁になる。 ただ、JSPI(WebAssembly.SuspendingとWebAssembly.promising)を使うと、wasm から JS のfetchを普通の関数のように呼んで待てた。Node とwrangler devの両方で、MoonBit の側は普通の import を宣言するだけで動いた。止まっているあいだに次の要求が入ってくるので、線形メモリの受け渡しは少し組み替える必要がある。本番の Workers ではまだ試していない。 - **ストリーミング。** 要求も応答も、まるごとメモリに置く。
詳しくは探索メモにまとめてある。