ch11-d1-replication
對應 11. D1 進階:Read Replication 與 Sessions API
在 GitHub 上檢視·5 個檔案·4.8 KB
取得並執行
這個範例可以獨立 clone 執行,不依賴其他章節。
npx degit mrsuner/cloudflare-serverless-platform-manuals/examples/ch11-d1-replication
cd ch11-d1-replication
npm install可用指令
npm run dev # wrangler dev
npm run typecheck # tsc --noEmit
npm run cf-typegen # wrangler types --env-interface CloudflareBindings說明
ch11 — D1 Sessions API
Section titled “ch11 — D1 Sessions API”Companion example for docs/11-d1-replication.md.
Replication itself cannot be exercised locally (one database, no replicas), but the Sessions API is fully present, so the code you ship can be written and type-checked here.
npm installnpx wrangler d1 migrations apply linkforge-demonpm run devB=localhost:8787curl -s "$B/session"curl -s "$B/constraints"curl -sD- "$B/write"curl -s "$B/read"curl -s -H "x-d1-bookmark: <bookmark from /write>" "$B/read"Verified findings
Section titled “Verified findings”2026-07-28 · wrangler@4.114.0 · workerd@1.20260722.1
Session shape and bookmark format
Section titled “Session shape and bookmark format”{"sessionProto":["constructor","_updateBookmark","prepare","batch","getBookmark",...], "bookmarkBeforeAnyQuery":null, "bookmarkAfterQuery":"00000000-00000003-00000000-00000000000000000000000000000000"}getBookmark() is null before the first query — handle it, or clients get
the string "null".
An invalid constraint is silently treated as a bookmark
Section titled “An invalid constraint is silently treated as a bookmark”{"first-unconstrained": {"ok":"00000000-00000004-00000000-0000...0000"}, "first-primary": {"ok":"00000000-00000005-00000000-0000...0000"}, "not-a-real-constraint":{"ok":"not-a-real-constraint"}, "(empty string)": {"ok":"00000000-00000007-00000000-0000...0000"}, "<bookmark-shaped string>":{"ok":"00000001-00000002-00004ce6-1234567890abcdef"}}withSession() takes constraints and bookmarks in the same string slot, so
"first-unconstrainted" raises nothing — it just becomes a bookmark and the
constraint you wanted never applies. TypeScript catches string literals; it
cannot catch values from headers, config or env.
Bookmarks advance monotonically
Section titled “Bookmarks advance monotonically”write -> x-d1-bookmark: 00000000-00000009-...write -> x-d1-bookmark: 00000000-0000000c-...Writes must go through the session too, or the bookmark never advances past them and later reads have nothing to catch up to.
What local dev can and cannot show
Section titled “What local dev can and cannot show”| local | production | |
|---|---|---|
withSession() / getBookmark() | yes | yes |
| monotonic bookmarks | yes | yes |
| constraint-string behaviour | yes | yes |
| actual replica routing | no | yes |
served_by_region / served_by_primary | absent | present |
| reproducing a stale read | no | yes |
Local meta only carries served_by: "miniflare.db". Verification is the
ratio of served_by_primary === true on non-write requests after deploying.
原始碼
wrangler.jsonc
{
"$schema": "./node_modules/wrangler/config-schema.json",
"name": "ch11-d1-replication",
"main": "src/index.ts",
"compatibility_date": "2026-07-24",
"observability": { "enabled": true },
"d1_databases": [{
"binding": "DB",
"database_name": "linkforge-demo",
"database_id": "00000000-0000-0000-0000-0000000d1011",
"migrations_dir": "./migrations"
}]
}package.json
{ "name": "ch11-d1-replication", "private": true, "type": "module",
"scripts": { "dev": "wrangler dev", "typecheck": "tsc --noEmit",
"cf-typegen": "wrangler types --env-interface CloudflareBindings" },
"devDependencies": { "typescript": "^5.9.0", "wrangler": "^4.114.0" } }src/index.ts
// ---------------------------------------------------------------------------
// Chapter 11 — D1 Sessions API.
//
// Replication itself cannot be exercised locally (there is one database and
// no replicas), but the SESSION API is fully present, so the code path you
// ship can be written and type-checked here.
// ---------------------------------------------------------------------------
const BOOKMARK_HEADER = "x-d1-bookmark";
const t = async (fn: () => Promise<unknown>): Promise<unknown> => {
try { return { ok: await fn() }; }
catch (e) { return { threw: String(e).slice(0, 200) }; }
};
export default {
async fetch(request: Request, env: CloudflareBindings): Promise<Response> {
const url = new URL(request.url);
switch (url.pathname) {
// What a session actually exposes, and what a bookmark looks like.
case "/session": {
const session = env.DB.withSession("first-unconstrained");
const beforeAnyQuery = session.getBookmark();
const r = await session.prepare("SELECT value FROM counters WHERE id = ?").bind("c1").all();
const afterQuery = session.getBookmark();
return Response.json({
sessionProto: Object.getOwnPropertyNames(Object.getPrototypeOf(session)),
bookmarkBeforeAnyQuery: beforeAnyQuery,
bookmarkAfterQuery: afterQuery,
meta: r.meta,
});
}
// Which constraint values are accepted?
case "/constraints": {
const out: Record<string, unknown> = {};
for (const c of ["first-unconstrained", "first-primary", "not-a-real-constraint", ""]) {
out[c || "(empty string)"] = await t(async () => {
const s = env.DB.withSession(c as never);
await s.prepare("SELECT 1 AS x").first();
return s.getBookmark();
});
}
// A bookmark string is also accepted in the same slot.
out["<bookmark-shaped string>"] = await t(async () => {
const s = env.DB.withSession("00000001-00000002-00004ce6-1234567890abcdef");
await s.prepare("SELECT 1 AS x").first();
return s.getBookmark();
});
return Response.json(out);
}
// The shape you actually ship: read the bookmark in, echo it back out.
case "/read": {
const incoming = request.headers.get(BOOKMARK_HEADER);
const session = env.DB.withSession(incoming ?? "first-unconstrained");
const row = await session
.prepare("SELECT value FROM counters WHERE id = ? LIMIT 1")
.bind("c1")
.first<{ value: number }>();
const res = Response.json({ value: row?.value ?? null, usedBookmark: incoming });
const b = session.getBookmark();
if (b) res.headers.set(BOOKMARK_HEADER, b);
return res;
}
// Writes must go through the session too, or the bookmark will not
// advance and a later read can legally serve a stale replica.
case "/write": {
const session = env.DB.withSession("first-primary");
await session
.prepare("UPDATE counters SET value = value + 1 WHERE id = ?")
.bind("c1")
.run();
const res = Response.json({ ok: true });
const b = session.getBookmark();
if (b) res.headers.set(BOOKMARK_HEADER, b);
return res;
}
// Same query without a session, for comparison.
case "/plain": {
const r = await env.DB.prepare("SELECT value FROM counters WHERE id = ? LIMIT 1")
.bind("c1").all();
return Response.json({ meta: r.meta });
}
default:
return new Response("try /session /constraints /read /write /plain\n", { status: 404 });
}
},
} satisfies ExportedHandler<CloudflareBindings>;migrations/0001_init.sql
CREATE TABLE IF NOT EXISTS counters (
id TEXT PRIMARY KEY,
value INTEGER NOT NULL DEFAULT 0
) STRICT;
INSERT OR IGNORE INTO counters (id, value) VALUES ('c1', 0);tsconfig.json
{ "compilerOptions": { "target": "esnext", "lib": ["esnext"], "module": "esnext",
"moduleResolution": "bundler", "types": ["./worker-configuration.d.ts"],
"strict": true, "skipLibCheck": true, "noEmit": true, "isolatedModules": true },
"include": ["src/**/*.ts", "worker-configuration.d.ts"] }