Workers + Durable Objectsで作るライブ投票実践

153 Views

September 24, 26

スライド概要

シェア

またはPlayer版

埋め込む »CMSなどでJSが使えない場合

ダウンロード

関連スライド

各ページのテキスト
1.

Workers + Durable Objects で作る ライブ投票 実践 なべっつ(@nabettu) ~ Cloudflare Workers実践LT 〜移行から運用、コストの話まで〜 2026/09/24 ~ 1

2.

自己紹介 なべっつ(@nabettu) X / GitHub: @nabettu Cloudflare Workers は 2020 年ごろから使っています イベント管理サービス(次のページで説明します) 空室の写真に AI で家具を置くサービス 2

3.

こういうサービスを作ってます 👇 イベントの告知・参加登録から、当日のライブ投票・Q&A・受付まで URL ひとつで ホスティング Cloudflare Workers(OpenNext)+ Smart Placement リアルタイム Durable Objects(WebSocket) ストレージ / キャッシュ R2 / KV(incremental cache) 定期実行 / 計測 Cron Triggers / Analytics Engine フレームワーク Next.js 16 (App Router) + TypeScript DB Turso (LibSQL) + Drizzle ORM 黄色いところが Cloudflare。月 $5(Workers Paid)で SSR もリアルタイムも全部載せ 3

4.

デモから始めます! 📱 ここから 投票してみてください! https://evet.jp/e/ibBUYnMp/live?tab=poll ログイン不要です。最後に結果を出します もし質問あれば、質問タブに書いてもらっても OK です 4

5.

いま動いているもの: ライブ投票 誰かが投票した瞬間、全員のグラフが動く(リロード不要) ログイン不要で投票できる(端末ごとの token で二重投票を防ぐ) 単一選択 / 複数選択、投票後に結果を発表、締め切り 同じ画面にライブ Q&A(質問 + いいね)も同居 この「全員の画面が同時に動く」を どう作ったかが今日の話です 5

6.

今日話すこと 1. Durable Objects ってなに? 2. 作り方(宣言 → つなぐ → 配る) 3. 設計の肝: 状態を持たせない 4. デプロイでハマった話 6

7.

Durable Objects は Workers の一機能 Worker 起動のしか アクセスごとに並列で何個でも立 た つ 状態 持てない(立つたびに別物) 向いてる用途 普通のリクエスト処理 Durable Object 名前ごとに世界で 1 つだけ立つ 持てる (同じ名前なら同じ 1 つに繋がる) 接続を 1 か所に束ねたいもの 別サービスの契約は不要。同じ wrangler deploy で一緒に出ます 7

8.

作り方 ① 宣言する wrangler.toml に 2 ブロック書くだけで生えます [[durable_objects.bindings]] name = "REALTIME_ROOM" class_name = "RealtimeRoom" [[migrations]] tag = "v1" new_sqlite_classes = ["RealtimeRoom"] 管理画面でポチポチしてリソースを作る作業はゼロ 8

9.
[beta]
作り方 ② つなぐ
// Worker

の fetch。WebSocket は SSR に渡す手前で横取り

if (request.headers.get("Upgrade") === "websocket") {
if (!(await authorize(request))) return new Response("ng", { status: 403 });
const id = env.REALTIME_ROOM.idFromName("qa:evt_123");
return env.REALTIME_ROOM.get(id).fetch(request);
}

idFromName("qa:<

部屋の割り当て
// ← 丸投げ
// ←

イベントID>") = イベントごとに 1 部屋。部屋割りはこれで終わり
9

10.
[beta]
DO 本体は 80 行
export class RealtimeRoom {
async fetch(request) {
if (request.headers.get("Upgrade") === "websocket") {
const [client, server] = Object.values(new WebSocketPair());
this.state.acceptWebSocket(server);
// ← Hibernation API
return new Response(null, { status: 101, webSocket: client });
}

サーバーからの配信を

const payload = await request.text();
//
for (const ws of this.state.getWebSockets()) ws.send(payload);
return new Response("ok");

//

全員へ

}
}

acceptWebSocket

= アイドル中は実行時間が課金されない

10

11.
[beta]
作り方 ③ 配る
const res = await voteLivePoll(eventId, pollId, optionIds);

に書いて
// 集計をそのまま配る

// DB

await broadcast(`qa:${eventId}`, "poll.vote.updated", {
pollId: res.pollId, counts: res.counts, totalVoters: res.totalVoters,
});

DO はドメインを何も知らないので、同じ 80 行を 3 機能で使い回し
ライブ投票 / ライブ Q&A( qa:<eventId> )
当日の受付チェックイン( reception:<eventId> )
11

12.

設計の肝: DO に状態を持たせない 持っているもの Durable Object 生きている接続だけ Turso (DB) 投票・質問・いいね・唯一のデータソース DO は状態を持てるけど、あえて持たせない(ただの pub/sub リレーに徹する) 12

13.

データが DB にあるから安心 デプロイで DO が再起動しても、失われるのは接続だけ データは 1 件も消えない 二重管理が構造的に起きない 「DO と DB、どっちが正しい?」が発生しない 配信は best-effort(失敗しても throw しない) WebSocket が塞がれた環境でも機能は成立。ライブ更新が止まるだけ リアルタイムが壊れても、データは壊れない 13

14.

ハマった話: デプロイのたびにエラー 173 件 症状: 本番デプロイごとにエラーアラートがバースト(173 件 / 15 分) 原因: 切断時に律儀に ws.close() を呼んでいた → デプロイ = DO 再起動 = 接続数に比例して例外 compatibility_flags = ["web_socket_auto_reply_to_close"] + ハンドラを空にするだけ(後始末すべき状態が無いから消せる) 14

15.

まとめ Durable Objects は Workers の一機能 — 別サービスを立てなくていい 状態は持たせない — DB を唯一のデータソースにする 切断は前提 — 再接続を最初から作る ぜひ DO を使ってみてください! 記事 → zenn.dev/nabettu サービス → evet.jp 15

16.

🗳 みなさんの投票結果 時間が余ったら、質問あれば答えます (ここでライブ投票の画面に切り替える。質問タブも見る) 16