コンテンツにスキップ

第16章: Durable Objects によるリアルタイム共同編集(実践チュートリアル)

一般的なサーバーレス関数(Workers)はリクエストごとに起動・終了するステートレス(状態を持たない)な設計ですが、Durable Objects (DO) を使うことで、世界中に1つだけの一意なインスタンス(アクター)を立ち上げ、インメモリ状態の保持、高パフォーマンスなリアルタイム通信、組み込みSQLiteによる永続化 を実現できます。

本章では、WebSocketハイバネーションAPI(待機時のCPU/メモリ消費0)を活用した 「リアルタイムチャット&メッセージ履歴永続化サーバー」 をゼロから構築します。


本チュートリアルで作成するもの

  • Durable Object (アクター):
    • 部屋(ルーム)ごとに1つのインスタンスが自動生成
    • 接続クライアントのWebSocket管理とメッセージのリアルタイム・ブロードキャスト
    • 組み込みSQLite(ctx.storage.sql)への過去ログ自動永続化
  • HTML/JSクライアント:
    • ブラウザからWebSocketで接続し、チャット送受信ができる検証用UI
  • CLI操作:
    • wrangler.jsonc でのマイグレーションタグ管理とローカル/本番デプロイ
【アーキテクチャ図】
[ ユーザーA (Browser) ] ──WebSocket──┐
├──> [ Durable Object (ChatRoom) ]
[ ユーザーB (Browser) ] ──WebSocket──┘ ├── メッセージ即時ブロードキャスト
└── 組み込みSQLiteに履歴保存

Step 1: プロジェクトの作成と設定

ターミナルで新しいプロジェクトを作成(または既存プロジェクトに設定)します。

Terminal window
mkdir do-chat-server && cd do-chat-server
npm init -y
npm install --save-dev wrangler typescript @cloudflare/workers-types

wrangler.jsonc を作成し、Durable Objects のバインディングと初期マイグレーションタグを設定します。

{
"$schema": "node_modules/wrangler/config-schema.json",
"name": "do-chat-server",
"main": "src/index.ts",
"compatibility_date": "2024-09-01",
"compatibility_flags": [
"nodejs_compat"
],
"durable_objects": {
"bindings": [
{
"name": "CHAT_ROOM",
"class_name": "ChatRoom"
}
]
},
// Durable Objects の初回作成時は migrations が必須
"migrations": [
{
"tag": "v1",
"new_classes": ["ChatRoom"]
}
]
}

型定義を自動生成します。

Terminal window
npx wrangler types

Step 2: Durable Object 実装コード(src/index.ts

src/index.ts を作成し、以下の完全コードを記述します。

import { DurableObject } from 'cloudflare:workers';
// メッセージの型定義
type ChatMessage = {
id?: number;
user: string;
text: string;
createdAt?: string;
};
// 1. Durable Object クラス(ルームごとに1つ存在)
export class ChatRoom extends DurableObject {
constructor(ctx: DurableObjectState, env: Env) {
super(ctx, env);
// 組み込みSQLiteの初期化(メッセージ履歴テーブル)
this.ctx.storage.sql.exec(`
CREATE TABLE IF NOT EXISTS messages (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user TEXT NOT NULL,
text TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT (datetime('now', 'utc'))
);
`);
}
// ルーターからのリクエスト受付
async fetch(request: Request): Promise<Response> {
const url = new URL(request.url);
// WebSocket接続のアップグレード要求
if (request.headers.get('Upgrade') === 'websocket') {
const pair = new WebSocketPair();
const [client, server] = Object.values(pair);
// WebSocketハイバネーションを受理(待機中のWorkerコストをゼロにする)
this.ctx.acceptWebSocket(server);
// 接続開始時に、過去のメッセージ履歴(最新10件)をクライアントへ送信
const cursor = this.ctx.storage.sql.exec<ChatMessage>(
'SELECT user, text, created_at as createdAt FROM messages ORDER BY id DESC LIMIT 10'
);
const history = Array.from(cursor).reverse();
server.send(JSON.stringify({ type: 'history', data: history }));
return new Response(null, { status: 101, webSocket: client });
}
return new Response('WebSocket endpoint only', { status: 400 });
}
// クライアントからメッセージを受信したときのハンドラー
async webSocketMessage(ws: WebSocket, message: string | ArrayBuffer) {
try {
const payload = JSON.parse(message.toString());
const { user, text } = payload;
if (!user || !text) return;
// 1. SQLiteデータベースへ保存
this.ctx.storage.sql.exec(
'INSERT INTO messages (user, text) VALUES (?, ?)',
user,
text
);
// 2. 接続中の全クライアントへリアルタイム転送(ブロードキャスト)
const broadcastData = JSON.stringify({
type: 'message',
data: { user, text, createdAt: new Date().toISOString() },
});
for (const client of this.ctx.getWebSockets()) {
client.send(broadcastData);
}
} catch (e) {
console.error('WebSocketメッセージ解析エラー:', e);
}
}
async webSocketClose(ws: WebSocket, code: number, reason: string) {
ws.close(code, 'Closed by DO');
}
}
// 2. メインのHTTPエントリーポイント(ルーティング)
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url);
// チャット接続エンドポイント: /ws?room=general
if (url.pathname === '/ws') {
const roomName = url.searchParams.get('room') || 'general';
// 部屋名から一意のDurable Object IDを取得(同じ名前なら世界中どこからでも同じインスタンスに接続)
const doId = env.CHAT_ROOM.idFromName(roomName);
const stub = env.CHAT_ROOM.get(doId);
return stub.fetch(request);
}
// クライアント用HTMLの配信(動作テスト用UI)
return new Response(htmlClient, {
headers: { 'Content-Type': 'text/html; charset=utf-8' },
});
}
};
// 動作確認用HTML
const htmlClient = `<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Durable Objects リアルタイムチャット</title>
<style>
body { font-family: sans-serif; max-width: 600px; margin: 30px auto; padding: 0 15px; }
#chat-box { border: 1px solid #ccc; height: 350px; overflow-y: scroll; padding: 10px; margin-bottom: 10px; border-radius: 6px; }
.msg { margin-bottom: 8px; }
.user { font-weight: bold; color: #f38020; }
.time { font-size: 0.8em; color: #888; margin-left: 6px; }
input, button { padding: 8px 12px; font-size: 1rem; }
</style>
</head>
<body>
<h2>⚡️ Durable Objects チャットルーム</h2>
<div id="chat-box"></div>
<input type="text" id="user" placeholder="お名前" value="User_${Math.floor(Math.random()*1000)}" style="width: 120px;">
<input type="text" id="text" placeholder="メッセージを入力..." style="width: 320px;">
<button onclick="send()">送信</button>
<script>
const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
const ws = new WebSocket(protocol + '//' + location.host + '/ws?room=general');
const box = document.getElementById('chat-box');
ws.onmessage = (e) => {
const res = JSON.parse(e.data);
if (res.type === 'history') {
res.data.forEach(appendMessage);
} else if (res.type === 'message') {
appendMessage(res.data);
}
};
function appendMessage(m) {
const div = document.createElement('div');
div.className = 'msg';
div.innerHTML = '<span class="user">' + m.user + ':</span> ' + m.text + '<span class="time">' + (m.createdAt || '') + '</span>';
box.appendChild(div);
box.scrollTop = box.scrollHeight;
}
function send() {
const user = document.getElementById('user').value;
const text = document.getElementById('text').value;
if (!text) return;
ws.send(JSON.stringify({ user, text }));
document.getElementById('text').value = '';
}
</script>
</body>
</html>`;

Step 3: ローカルでの動作検証

ローカル開発サーバーを起動します。

Terminal window
npx wrangler dev
  1. ブラウザを 2つのウィンドウ で開き、http://localhost:8787 にアクセスします。
  2. 片方のウィンドウでメッセージを入力して「送信」を押します。
  3. もう片方のウィンドウに 瞬時(数ミリ秒) でメッセージが表示されることを確認します。
  4. ページをリロードしても、組み込みSQLiteに過去ログが保存されているため履歴が表示されます。

Step 4: 本番デプロイ

Terminal window
npx wrangler deploy

出力された https://do-chat-server.xxxx.workers.dev にアクセスすれば、世界中誰とでもリアルタイムにチャットが行えます。


まとめ

  • アクターモデル: idFromName('general') で世界に1つだけの常駐オブジェクトを特定。
  • WebSocketハイバネーション: 何千人が接続していても、発言がない待機中のCPU課金はゼロ。
  • SQLite統合: 外部DBへの通信ラグなしで、インスタンス内で超高速にメッセージ履歴を永続化。

💡 用語解説コラム

[!NOTE] アクターモデル (Actor Model)
並行コンピューティングの設計モデル。Durable Objectsのように「個別の状態(メモリ/DB)を持ち、メッセージ送受信でのみやり取りする自律した単位(アクター)」として振る舞います。

[!NOTE] WebSocketハイバネーション (Hibernation)
クライアントとWebSocket接続を維持したまま、通信がない待機中はWorkerプロセスのメモリとCPU消費をゼロにする技術。接続維持コストを劇的に下げます。