問わない — ポーリングが無駄であることより悪い理由

5 分
目標MCP クライアント講座の最終編。購読し、通知を受け、そのとき読む。ポーリングが資源を食うのは副次的だ — 本当の問題は、二度問う間に値が二度変われば真ん中を永遠に見られないことである。

4 編目まで、最新の値を知るには問わねばならなかった。一秒ごとに問うか、人が更新するか。

購読する

client.onResourceUpdated((uri) {
  // The notification says only that it changed. Read to learn what to.
  seen.add(uri);
  stdout.writeln('notified: $uri');
});

await client.subscribeResource('desk://waiting');

そして値が変わるようにしてみる。

await client.callTool('desk.admit', {'count': 1});
subscribed to desk://waiting
notified: desk://waiting
after the notification, reading gives {waiting: 2}
notifications received: 1

通知は uri しか運ばない

ハンドラが受けるのは uri ひとつだ。何人になったかは来ない。サーバー 6 編目で値を載せない理由を扱ったが、受ける側ではそれが 読まねばならない という意味になる。

final now = await client.readResource('desk://waiting');

もう一往復するのが損に見えるが、これが常に正しい。通知が何個来ようと順序がどうであろうと、読んだ時点の値 が最新だからだ。

ポーリングの本当の問題

ポーリングを避ける理由として、たいてい資源が挙げられる。一秒ごとにサーバーを叩けば無駄だ。

それは副次的である。本当の問題は、二度問う間を見られないことだ。

09:00:00    ポーリング → waiting 3
09:00:00.4  二人入場 → waiting 1
09:00:00.7  一人到着 → waiting 2
09:00:01    ポーリング → waiting 2

画面は 3 から 2 へ行った。1 だった瞬間を見ていない。 最終値だけが必要なら構わないが、「0 になったら通知」のような規則があれば、その規則は発動しない。

間隔を縮めても消えない。狭くなるだけだ。そして狭めるほど無駄が増える。

終わったら解く

await client.unsubscribeResource('desk://waiting');

画面が閉じたら購読を解く。解かなければサーバーが居ない相手に送り続ける。

実機の編の ESP32 はこれを 画面定義の中に 入れていた — onReady で購読、onDestroy で解除。生命周期が画面に書かれていれば、クライアントは忘れられない。

検証 — ポーリングなら失敗

grep -q 'notified: desk://waiting' captures/s5.txt || die "no notification arrived"
grep -q 'notifications received: 1' captures/s5.txt || die "wrong notification count"
grep -q 'reading gives {waiting: 2}' captures/s5.txt || die "the read after the notification was stale"

# Polling would show up as a timer; the claim is that there is none.
grep -qE 'Timer|periodic' bin/step5.dart && die "it is polling, not subscribing"

Timer や periodic がコードにあれば失敗する。結果だけを見ればポーリングと購読は同じに見えるから だ — どちらも画面に 2 が出る。

通知の個数を数えるのも同じ理由だ。0 なら来ていないし、複数なら重複購読である。

五編が終わった

step1  connected to Course 1.0.0
step2  tools: desk.admit
       resources: ui://desk, desk://waiting
step3  admit 1 -> {waiting: 2}
       admit 99 -> isError=true "only 2 waiting"
step4  ui://desk — 546 B, 6 lines, type "page", title "Desk"
step5  notifications received: 1 · after the notification, reading gives {waiting: 2}

5 steps · each checked against its own claim · driven against the course server

モックのサーバーが無い。 五段階すべて、サーバー講座で作ったあのサーバーを実際に立てて繋ぐ。だから二つのトラックがずれればここで引っかかる — 実際に 3 編目で一度引っかかった。

自分で動かす

cd content/sample/course-client
dart pub get
bash verify.sh            # 五段階すべて
dart run bin/step5.dart

持ち帰るもの

  1. onResourceUpdated + subscribeResource — ハンドラを先に掛けてから購読する
  2. 通知の後に読む — 通知は uri しか運ばない
  3. ポーリング禁止の検査 — 結果が同じに見えるのでコードを見る

次のトラック

サーバーとクライアントが立った。次は画面そのもの — JSON のひと塊がどうやって画面になるのか。

サンプルを実行する

makemind-academy/course_client/
git clone https://github.com/makemind-academy/course_client
cd course_client
dart pub get
(cd course-server && dart pub get)
dart run bin/step5.dart
GitHub で開く →