Skip to content

Commit 6d2179e

Browse files
committed
Serialize Cloudflare WordPress state revisions
1 parent f6c8c67 commit 6d2179e

4 files changed

Lines changed: 117 additions & 3 deletions

File tree

packages/runtime-cloudflare/src/php-wasm-universal.d.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
declare module "@php-wasm/universal" {
22
export class PHP {
33
constructor(runtimeId: number)
4-
run(request: { code: string }): Promise<{ text: string }>
4+
run(request: { code: string }): Promise<{ bytes: Uint8Array; text: string }>
55
mkdir(path: string): void
6+
readFileAsBuffer(path: string): Uint8Array
67
writeFile(path: string, data: Uint8Array): void
78
exit(code?: number): void
89
}

packages/runtime-cloudflare/src/worker.ts

Lines changed: 95 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,26 @@ const WORDPRESS_ARCHIVE_URL = "https://wordpress.org/latest.zip"
1313
const SQLITE_INTEGRATION_ARCHIVE_URL = "https://github.com/WordPress/sqlite-database-integration/releases/download/v2.2.23/plugin-sqlite-database-integration.zip"
1414
const SITE_URL = "https://wp-codebox-runtime.invalid"
1515
const DATABASE_PATH = "/wordpress/wp-content/database/.ht.sqlite"
16+
const R2_DATABASE_POINTER_KEY = "sites/default/database/current.json"
17+
const R2_DATABASE_REVISION_PREFIX = "sites/default/database/revisions"
1618
let bootPromise: Promise<{ php: PHP; wordpressVersion: string }> | undefined
1719

20+
interface Env {
21+
WORDPRESS_STATE: DurableObjectNamespace
22+
WORDPRESS_STATE_BUCKET: R2Bucket
23+
}
24+
1825
export default {
19-
async fetch(request: Request): Promise<Response> {
26+
async fetch(request: Request, env: Env): Promise<Response> {
2027
const phase = new URL(request.url).searchParams.get("phase")
28+
if (phase === "r2-state" || phase === "r2-mutate") {
29+
const expectedMethod = phase === "r2-mutate" ? "POST" : "GET"
30+
if (request.method !== expectedMethod) {
31+
return new Response(`WordPress state ${phase === "r2-mutate" ? "mutation" : "read"} requires ${expectedMethod}.`, { status: 405 })
32+
}
33+
const coordinator = env.WORDPRESS_STATE.getByName("default")
34+
return coordinator.fetch(request)
35+
}
2136
if (phase) return runBootProbe(phase)
2237

2338
const runtime = await (bootPromise ??= bootWordPressRuntime(
@@ -38,6 +53,85 @@ export default {
3853
},
3954
}
4055

56+
interface DatabasePointer {
57+
revision: string
58+
databaseKey: string
59+
persistedAt: string
60+
}
61+
62+
export class WordPressStateCoordinator implements DurableObject {
63+
private tail: Promise<void> = Promise.resolve()
64+
65+
constructor(
66+
private readonly state: DurableObjectState,
67+
private readonly env: Env,
68+
) {}
69+
70+
fetch(request: Request): Promise<Response> {
71+
const response = this.tail.then(() => this.handleRequest(request))
72+
this.tail = response.then(() => undefined, () => undefined)
73+
return response
74+
}
75+
76+
private async handleRequest(request: Request): Promise<Response> {
77+
if (request.method === "GET") {
78+
const pointer = await this.readPointer()
79+
return Response.json({
80+
schema: "wp-codebox/cloudflare-wordpress-state/v1",
81+
durableObjectId: this.state.id.toString(),
82+
pointer,
83+
})
84+
}
85+
if (request.method !== "POST") return new Response("Method not allowed.", { status: 405 })
86+
87+
const pointer = await this.readPointer()
88+
const persistedDatabase = pointer ? await this.env.WORDPRESS_STATE_BUCKET.get(pointer.databaseKey) : null
89+
if (pointer && !persistedDatabase) {
90+
throw new Error(`R2 database revision is missing: ${pointer.databaseKey}`)
91+
}
92+
93+
const database = persistedDatabase
94+
? new Uint8Array(await persistedDatabase.arrayBuffer())
95+
: new Uint8Array(wordpressInstallSeed)
96+
const runtime = await bootWordPressRuntime("do-not-attempt-installing", true, true, database)
97+
try {
98+
const mutationOutput = (await runtime.php.run({
99+
code: "<?php require '/wordpress/wp-load.php'; $value = (int) get_option('wp_codebox_r2_revision', 0) + 1; update_option('wp_codebox_r2_revision', $value); echo json_encode(['revisionValue' => $value, 'wordpressVersion' => get_bloginfo('version')]);",
100+
})).text.trim()
101+
const mutation = JSON.parse(mutationOutput) as { revisionValue: number; wordpressVersion: string }
102+
const revision = crypto.randomUUID()
103+
const databaseKey = `${R2_DATABASE_REVISION_PREFIX}/${revision}.sqlite`
104+
const persistedAt = new Date().toISOString()
105+
const databaseBytes = runtime.php.readFileAsBuffer(DATABASE_PATH)
106+
await this.env.WORDPRESS_STATE_BUCKET.put(databaseKey, databaseBytes, {
107+
customMetadata: {
108+
persistedAt,
109+
revisionValue: String(mutation.revisionValue),
110+
wordpressVersion: mutation.wordpressVersion,
111+
},
112+
})
113+
114+
const nextPointer: DatabasePointer = { revision, databaseKey, persistedAt }
115+
await this.env.WORDPRESS_STATE_BUCKET.put(R2_DATABASE_POINTER_KEY, JSON.stringify(nextPointer), {
116+
httpMetadata: { contentType: "application/json" },
117+
})
118+
return Response.json({
119+
schema: "wp-codebox/cloudflare-wordpress-mutation/v1",
120+
source: pointer ? "r2-revision" : "packaged-seed",
121+
...mutation,
122+
pointer: nextPointer,
123+
})
124+
} finally {
125+
runtime.php.exit()
126+
}
127+
}
128+
129+
private async readPointer(): Promise<DatabasePointer | null> {
130+
const object = await this.env.WORDPRESS_STATE_BUCKET.get(R2_DATABASE_POINTER_KEY)
131+
return object ? object.json<DatabasePointer>() : null
132+
}
133+
}
134+
41135
async function runBootProbe(phase: string): Promise<Response> {
42136
if (phase === "wordpress-archive" || phase === "sqlite-archive") {
43137
const archive = phase === "wordpress-archive"

packages/runtime-cloudflare/wrangler.jsonc

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,17 @@
44
"compatibility_date": "2026-07-18",
55
"compatibility_flags": ["nodejs_compat"],
66
"limits": { "cpu_ms": 300000 },
7+
"r2_buckets": [
8+
{ "binding": "WORDPRESS_STATE_BUCKET", "bucket_name": "wp-codebox-runtime-chubes" }
9+
],
10+
"durable_objects": {
11+
"bindings": [
12+
{ "name": "WORDPRESS_STATE", "class_name": "WordPressStateCoordinator" }
13+
]
14+
},
15+
"migrations": [
16+
{ "tag": "v1", "new_sqlite_classes": ["WordPressStateCoordinator"] }
17+
],
718
"rules": [
819
{ "type": "CompiledWasm", "globs": ["**/*.wasm"], "fallthrough": false },
920
{ "type": "Data", "globs": ["**/*.sqlite"], "fallthrough": false }

tests/cloudflare-runtime.test.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,17 @@ test("Cloudflare runtime declares the paid-plan WordPress boot CPU budget", asyn
3030
})
3131

3232
test("Cloudflare runtime packages the disposable WordPress install seed", async () => {
33-
const config = JSON.parse(await readFile(new URL("../packages/runtime-cloudflare/wrangler.jsonc", import.meta.url), "utf8")) as { rules?: Array<{ type?: string; globs?: string[] }> }
33+
const config = JSON.parse(await readFile(new URL("../packages/runtime-cloudflare/wrangler.jsonc", import.meta.url), "utf8")) as {
34+
rules?: Array<{ type?: string; globs?: string[] }>
35+
r2_buckets?: Array<{ binding?: string; bucket_name?: string }>
36+
durable_objects?: { bindings?: Array<{ name?: string; class_name?: string }> }
37+
migrations?: Array<{ new_sqlite_classes?: string[] }>
38+
}
3439
const seed = await readFile(new URL("../packages/runtime-cloudflare/assets/wordpress-install-seed.sqlite", import.meta.url))
3540

3641
assert.equal(seed.subarray(0, 16).toString(), "SQLite format 3\0")
3742
assert.ok(config.rules?.some((rule) => rule.type === "Data" && rule.globs?.includes("**/*.sqlite")))
43+
assert.deepEqual(config.r2_buckets, [{ binding: "WORDPRESS_STATE_BUCKET", bucket_name: "wp-codebox-runtime-chubes" }])
44+
assert.deepEqual(config.durable_objects?.bindings, [{ name: "WORDPRESS_STATE", class_name: "WordPressStateCoordinator" }])
45+
assert.ok(config.migrations?.some((migration) => migration.new_sqlite_classes?.includes("WordPressStateCoordinator")))
3846
})

0 commit comments

Comments
 (0)