SKILL.md
readonlyread-only
name
durable-objects
description
建立與審查 Cloudflare Durable Objects。適用於建構有狀態的協調(聊天室、多人遊戲、預約系統)、實作 RPC 方法、SQLite 儲存、鬧鐘、WebSocket,或審查 DO 程式碼以符合最佳實務。涵蓋 Workers 整合、wrangler 設定,以及使用 Vitest 進行測試。傾向從 Cloudflare 文件檢索而非依賴預先訓練的知識。
Durable Objects
在 Cloudflare 邊緣建構有狀態、協調的應用程式,使用 Durable Objects。
檢索來源
你對 Durable Objects API 和設定的知識可能已過時。對於任何 Durable Objects 任務,優先檢索而非預先訓練。
實作功能時,請擷取相關的文件頁面。
使用時機
- 建立新的 Durable Object 類別以進行有狀態協調
- 實作 RPC 方法、鬧鐘或 WebSocket 處理器
- 審查現有 DO 程式碼是否符合最佳實務
- 設定 wrangler.jsonc/toml 中的 DO 綁定與遷移
- 使用
@cloudflare/vitest-pool-workers撰寫測試 - 設計分片策略與父子關係
參考文件
./references/rules.md- 核心規則、儲存、並行、RPC、鬧鐘./references/testing.md- Vitest 設定、單元/整合測試、鬧鐘測試./references/workers.md- Workers 處理器、型別、wrangler 設定、可觀測性
搜尋:blockConcurrencyWhile、idFromName、getByName、setAlarm、sql.exec
核心原則
使用 Durable Objects 的情境
| 需求 | 範例 |
|---|---|
| 協調 | 聊天室、多人遊戲、協作文件 |
| 強一致性 | 庫存、預約系統、回合制遊戲 |
| 每個實體的儲存 | 多租戶 SaaS、每個使用者的資料 |
| 持久連線 | WebSocket、即時通知 |
| 每個實體的排程工作 | 訂閱續約、遊戲超時 |
請勿使用的情境
- 無狀態請求處理(使用一般 Workers)
- 需要最大全球分佈
- 高扇出獨立請求
快速參考
Wrangler 設定
// wrangler.jsonc
{
"durable_objects": {
"bindings": [{ "name": "MY_DO", "class_name": "MyDurableObject" }]
},
"migrations": [{ "tag": "v1", "new_sqlite_classes": ["MyDurableObject"] }]
}
基本 Durable Object 模式
import { DurableObject } from "cloudflare:workers";
export interface Env {
MY_DO: DurableObjectNamespace<MyDurableObject>;
}
export class MyDurableObject extends DurableObject<Env> {
constructor(ctx: DurableObjectState, env: Env) {
super(ctx, env);
ctx.blockConcurrencyWhile(async () => {
this.ctx.storage.sql.exec(`
CREATE TABLE IF NOT EXISTS items (
id INTEGER PRIMARY KEY AUTOINCREMENT,
data TEXT NOT NULL
)
`);
});
}
async addItem(data: string): Promise<number> {
const result = this.ctx.storage.sql.exec<{ id: number }>(
"INSERT INTO items (data) VALUES (?) RETURNING id",
data
);
return result.one().id;
}
}
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const stub = env.MY_DO.getByName("my-instance");
const id = await stub.addItem("hello");
return Response.json({ id });
},
};
關鍵規則
- 以協調原子為模型 - 每個聊天室/遊戲/使用者一個 DO,而非一個全域 DO
- 使用
getByName()進行確定性路由 - 相同輸入 = 相同 DO 實例 - 使用 SQLite 儲存 - 在遷移中設定
new_sqlite_classes - 在建構子中初始化 - 僅使用
blockConcurrencyWhile()進行結構設定 - 使用 RPC 方法 - 而非 fetch() 處理器(相容日期 >= 2024-04-03)
- 先持久化,再快取 - 在更新記憶體狀態前,務必先寫入儲存
- 每個 DO 一個鬧鐘 -
setAlarm()會取代任何現有鬧鐘
反模式(絕對不要)
- 單一全域 DO 處理所有請求(瓶頸)
- 在每個請求上使用
blockConcurrencyWhile()(扼殺吞吐量) - 僅將關鍵狀態儲存在記憶體中(在回收/崩潰時遺失)
- 在相關的儲存寫入之間使用
await(破壞原子性) - 在
fetch()或外部 I/O 期間持有blockConcurrencyWhile()
Stub 建立
// 確定性 - 大多數情況較佳
const stub = env.MY_DO.getByName("room-123");
// 從現有 ID 字串
const id = env.MY_DO.idFromString(storedIdString);
const stub = env.MY_DO.get(id);
// 新的唯一 ID - 在外部儲存對應關係
const id = env.MY_DO.newUniqueId();
const stub = env.MY_DO.get(id);
儲存操作
// SQL(同步,建議使用)
this.ctx.storage.sql.exec("INSERT INTO t (c) VALUES (?)", value);
const rows = this.ctx.storage.sql.exec<Row>("SELECT * FROM t").toArray();
// KV(非同步)
await this.ctx.storage.put("key", value);
const val = await this.ctx.storage.get<Type>("key");
鬧鐘
// 排程(取代現有)
await this.ctx.storage.setAlarm(Date.now() + 60_000);
// 處理器
async alarm(): Promise<void> {
// 處理排程工作
// 可選擇重新排程:await this.ctx.storage.setAlarm(...)
}
// 取消
await this.ctx.storage.deleteAlarm();
測試快速入門
import { env } from "cloudflare:test";
import { describe, it, expect } from "vitest";
describe("MyDO", () => {
it("應該正常運作", async () => {
const stub = env.MY_DO.getByName("test");
const result = await stub.addItem("test");
expect(result).toBe(1);
});
});






