Skip to content

Commit c4fbc71

Browse files
committed
Add bounded Cloudflare editor probes
1 parent 1f08ffe commit c4fbc71

3 files changed

Lines changed: 57 additions & 1 deletion

File tree

packages/runtime-cloudflare/src/request-routing.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,11 @@ export type WorkerRequestRoute =
55
| { kind: "r2-mutate" }
66
| { kind: "canonical-auth" }
77
| { kind: "operator-reset" }
8+
| { kind: "editor-probe"; phase: EditorProbePhase }
89
| { kind: "probe"; phase: string }
910

11+
export type EditorProbePhase = "admin" | "auto-draft" | "block-editor"
12+
1013
export function routeWorkerRequest(request: Request): WorkerRequestRoute {
1114
const phase = new URL(request.url).searchParams.get("phase")
1215
if (phase === null) return { kind: "wordpress" }
@@ -15,5 +18,8 @@ export function routeWorkerRequest(request: Request): WorkerRequestRoute {
1518
if (phase === "r2-mutate") return { kind: "r2-mutate" }
1619
if (phase === "canonical-auth") return { kind: "canonical-auth" }
1720
if (phase === "operator-reset") return { kind: "operator-reset" }
21+
if (phase === "editor-probe-admin") return { kind: "editor-probe", phase: "admin" }
22+
if (phase === "editor-probe-auto-draft") return { kind: "editor-probe", phase: "auto-draft" }
23+
if (phase === "editor-probe-block-editor") return { kind: "editor-probe", phase: "block-editor" }
1824
return { kind: "probe", phase }
1925
}

packages/runtime-cloudflare/src/worker.ts

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { dependenciesTotalSize, init } from "../../../node_modules/@php-wasm/web
77
import phpWasmModule from "../../../node_modules/@php-wasm/web-8-5/asyncify/8_5_8/php_8_5.wasm"
88
import { CLOUDFLARE_RUNTIME_HEALTH_MARKER, CLOUDFLARE_RUNTIME_HEALTH_SCHEMA, cloudflareRuntimeHealthResponse } from "./health-envelope.js"
99
import { leaseRetryDelayMs } from "./lease-retry.js"
10-
import { routeWorkerRequest } from "./request-routing.js"
10+
import { routeWorkerRequest, type EditorProbePhase } from "./request-routing.js"
1111
import { toFetchResponse, toPHPRequest } from "./request-translation.js"
1212
import { deriveWordPressAuthConstants, type WordPressAuthConstant } from "./wordpress-auth.js"
1313
import { isWordPressRuntimeFile, wordpressStaticArchivePath, wordpressStaticContentType } from "./wordpress-runtime-corpus.js"
@@ -86,6 +86,7 @@ export default {
8686
const route = routeWorkerRequest(request)
8787
const coordinator = env.WORDPRESS_STATE.getByName("default")
8888
if (route.kind === "operator-reset") return resetCanonicalWordPress(request, env, coordinator)
89+
if (route.kind === "editor-probe") return runCoordinatedEditorProbe(request, env, coordinator, route.phase)
8990
if (route.kind === "probe") {
9091
if (route.phase === "canonical-session") return canonicalSessionProbe(env.WORDPRESS_STATE_BUCKET, await coordinatorCall<{ pointer: MarkdownPointer | null }>(coordinator, request.url, "state"))
9192
return runBootProbe(route.phase, env.WORDPRESS_STATE_BUCKET)
@@ -99,6 +100,41 @@ export default {
99100
},
100101
}
101102

103+
async function runCoordinatedEditorProbe(request: Request, env: Env, coordinator: DurableObjectStub, phase: EditorProbePhase): Promise<Response> {
104+
const lease = await acquireLease(coordinator, request.url)
105+
let finalized = false
106+
try {
107+
if (!lease.pointer) throw new Error("Canonical WordPress must be initialized before running an editor probe.")
108+
const runtime = await getRuntime(env, lease.pointer, new URL(request.url).origin)
109+
patchEditorProbe(runtime.php, phase)
110+
const response = toFetchResponse(request, await runtime.requestHandler.request(await toPHPRequest(request)))
111+
await releaseLease(coordinator, request.url, lease)
112+
finalized = true
113+
await discardCachedRuntime()
114+
return response
115+
} catch (error) {
116+
if (!finalized) await abortLease(coordinator, request.url, lease)
117+
await discardCachedRuntime()
118+
throw error
119+
}
120+
}
121+
122+
function patchEditorProbe(php: PHP, phase: EditorProbePhase): void {
123+
const path = "/wordpress/wp-admin/post-new.php"
124+
const source = new TextDecoder().decode(php.readFileAsBuffer(path))
125+
const markers: Record<EditorProbePhase, string> = {
126+
admin: "require_once __DIR__ . '/admin.php';",
127+
"auto-draft": "$post_ID = $post->ID;",
128+
"block-editor": "require_once ABSPATH . 'wp-admin/admin-footer.php';",
129+
}
130+
const marker = markers[phase]
131+
const index = source.indexOf(marker)
132+
if (index === -1 || index !== source.lastIndexOf(marker)) throw new Error(`WordPress editor probe marker is not unique: ${phase}`)
133+
const stop = `\necho wp_json_encode( array( 'schema' => 'wp-codebox/cloudflare-editor-probe/v1', 'phase' => '${phase}', 'memoryBytes' => memory_get_usage( true ), 'peakMemoryBytes' => memory_get_peak_usage( true ) ) );\nreturn;`
134+
const insertion = phase === "block-editor" ? index : index + marker.length
135+
php.writeFile(path, new TextEncoder().encode(`${source.slice(0, insertion)}${stop}${source.slice(insertion)}`))
136+
}
137+
102138
interface MarkdownManifestFile {
103139
path: string
104140
objectKey: string

tests/cloudflare-runtime.test.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,9 @@ test("Cloudflare routing reserves phases while the phase-less route serves WordP
4343
assert.deepEqual(routeWorkerRequest(new Request("https://worker.example/?phase=r2-mutate")), { kind: "r2-mutate" })
4444
assert.deepEqual(routeWorkerRequest(new Request("https://worker.example/?phase=canonical-auth")), { kind: "canonical-auth" })
4545
assert.deepEqual(routeWorkerRequest(new Request("https://worker.example/?phase=operator-reset")), { kind: "operator-reset" })
46+
assert.deepEqual(routeWorkerRequest(new Request("https://worker.example/wp-admin/post-new.php?phase=editor-probe-admin")), { kind: "editor-probe", phase: "admin" })
47+
assert.deepEqual(routeWorkerRequest(new Request("https://worker.example/wp-admin/post-new.php?phase=editor-probe-auto-draft")), { kind: "editor-probe", phase: "auto-draft" })
48+
assert.deepEqual(routeWorkerRequest(new Request("https://worker.example/wp-admin/post-new.php?phase=editor-probe-block-editor")), { kind: "editor-probe", phase: "block-editor" })
4649
assert.deepEqual(routeWorkerRequest(new Request("https://worker.example/?phase=seeded-wordpress")), { kind: "probe", phase: "seeded-wordpress" })
4750
})
4851

@@ -114,6 +117,17 @@ test("Cloudflare lease contention honors Retry-After without exceeding the acqui
114117
assert.equal(leaseRetryDelayMs(Number.NaN, 500), 500)
115118
})
116119

120+
test("Cloudflare editor probes bound the authenticated editor lifecycle and discard their runtime", async () => {
121+
const worker = await readFile(new URL("../packages/runtime-cloudflare/src/worker.ts", import.meta.url), "utf8")
122+
const probe = worker.slice(worker.indexOf("async function runCoordinatedEditorProbe"), worker.indexOf("async function resetCanonicalWordPress"))
123+
124+
assert.match(probe, /await runtime\.requestHandler\.request/)
125+
assert.match(probe, /await releaseLease/)
126+
assert.equal((probe.match(/await discardCachedRuntime\(\)/g) ?? []).length, 2)
127+
assert.match(probe, /\/wordpress\/wp-admin\/post-new\.php/)
128+
assert.match(probe, /memory_get_peak_usage/)
129+
})
130+
117131
test("Cloudflare runtime packages a provenanced canonical MDI seed", async () => {
118132
const config = JSON.parse((await readFile(new URL("../packages/runtime-cloudflare/wrangler.jsonc", import.meta.url), "utf8")).replace(/^\s*\/\/.*\n/, "")) as {
119133
rules?: Array<{ type?: string; globs?: string[] }>

0 commit comments

Comments
 (0)