Skip to content

Commit 5abafae

Browse files
committed
Serve cached Cloudflare pages before runtime boot
1 parent f3963f7 commit 5abafae

2 files changed

Lines changed: 65 additions & 17 deletions

File tree

packages/runtime-cloudflare/src/worker.ts

Lines changed: 58 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ const MARKDOWN_RESOLVED_INDEX_PATH = "/tmp/markdown-index-8133b4cf3c66.sqlite"
3434
const MARKDOWN_CHANGES_PATH = "/tmp/wp-codebox-canonical-changes.json"
3535
const R2_MARKDOWN_REVISION_PREFIX = "sites/default/markdown/revisions"
3636
const R2_MARKDOWN_OBJECT_PREFIX = "sites/default/markdown/objects"
37+
const R2_WORDPRESS_PAGE_PREFIX = "sites/default/pages"
38+
const WORDPRESS_PAGE_CACHE_SCHEMA = "v2"
3739
const SERIALIZED_MARKDOWN_MUTATION_CODE = `<?php
3840
define('SHORTINIT', true);
3941
require '/wordpress/wp-load.php';
@@ -114,6 +116,18 @@ interface RuntimeFile {
114116
bytes: Uint8Array
115117
}
116118

119+
interface CoordinatorState {
120+
pointer: MarkdownPointer | null
121+
}
122+
123+
interface WordPressPageSnapshot {
124+
schema: "wp-codebox/wordpress-page/v1"
125+
status: number
126+
statusText: string
127+
headers: Array<[string, string]>
128+
body: string
129+
}
130+
117131
interface CanonicalSeedManifest {
118132
schema: string
119133
markdownDatabaseIntegrationRevision: string
@@ -201,6 +215,13 @@ async function secretsMatch(left: string, right: string): Promise<boolean> {
201215

202216
async function runCoordinatedWordPressRequest(request: Request, env: Env, coordinator: DurableObjectStub, route: "wordpress" | "health" | "r2-mutate"): Promise<Response> {
203217
if (route === "r2-mutate" && request.method !== "POST") return new Response("WordPress state mutation requires POST.", { status: 405 })
218+
if (route === "wordpress" && isCacheableWordPressPageRequest(request)) {
219+
const state = await coordinatorCall<CoordinatorState>(coordinator, request.url, "state")
220+
if (state.pointer) {
221+
const cachedPage = await matchWordPressPageCache(request, state.pointer, env.WORDPRESS_STATE_BUCKET)
222+
if (cachedPage) return cachedPage
223+
}
224+
}
204225
let lease = await acquireLease(coordinator, request.url)
205226
let runtime: Runtime | undefined
206227
let finalized = false
@@ -212,7 +233,7 @@ async function runCoordinatedWordPressRequest(request: Request, env: Env, coordi
212233
if (!lease.pointer || lease.pointer.revision !== bootstrapped.pointer.revision) throw new Error("Canonical bootstrap promotion was not observed by its next lease.")
213234
cacheRuntime(lease.pointer, bootstrapped)
214235
}
215-
const cachedPage = route === "wordpress" ? await matchWordPressPageCache(request, lease.pointer) : null
236+
const cachedPage = route === "wordpress" ? await matchWordPressPageCache(request, lease.pointer, env.WORDPRESS_STATE_BUCKET) : null
216237
if (cachedPage) {
217238
await releaseLease(coordinator, request.url, lease)
218239
finalized = true
@@ -242,7 +263,7 @@ async function runCoordinatedWordPressRequest(request: Request, env: Env, coordi
242263
}
243264
finalized = true
244265
if (mutatesCanonicalState) await discardRuntime(runtime)
245-
else if (route === "wordpress") response = await cacheWordPressPage(request, lease.pointer, response)
266+
else if (route === "wordpress") response = await cacheWordPressPage(request, lease.pointer, response, env.WORDPRESS_STATE_BUCKET)
246267
return response
247268
} catch (error) {
248269
if (!finalized) await abortLease(coordinator, request.url, lease)
@@ -259,43 +280,65 @@ function isCacheableWordPressPageRequest(request: Request): boolean {
259280
return !url.searchParams.has("preview") && !url.searchParams.has("rest_route")
260281
}
261282

262-
async function matchWordPressPageCache(request: Request, pointer: MarkdownPointer): Promise<Response | null> {
283+
async function matchWordPressPageCache(request: Request, pointer: MarkdownPointer, bucket: R2Bucket): Promise<Response | null> {
263284
if (!isCacheableWordPressPageRequest(request)) return null
264285
const cache = typeof caches === "undefined" ? undefined : (caches as CacheStorage & { default?: Cache }).default
265-
if (!cache) return null
266286
try {
267-
const cached = await cache.match(wordPressPageCacheKey(request, pointer))
268-
return cached ? pageCacheResponse(cached, request.method === "HEAD", "hit") : null
287+
const cached = cache ? await cache.match(wordPressPageCacheKey(request, pointer)) : null
288+
if (cached) return pageCacheResponse(cached, request.method === "HEAD", "hit", "edge")
289+
const object = await bucket.get(await wordPressPageSnapshotKey(request, pointer))
290+
if (!object) return null
291+
const snapshot = JSON.parse(await object.text()) as WordPressPageSnapshot
292+
if (snapshot.schema !== "wp-codebox/wordpress-page/v1" || snapshot.status !== 200 || !Array.isArray(snapshot.headers) || typeof snapshot.body !== "string") return null
293+
const response = new Response(snapshot.body, { status: snapshot.status, statusText: snapshot.statusText, headers: snapshot.headers })
294+
if (cache) await cache.put(wordPressPageCacheKey(request, pointer), response.clone())
295+
return pageCacheResponse(response, request.method === "HEAD", "hit", "r2")
269296
} catch {
270297
return null
271298
}
272299
}
273300

274-
async function cacheWordPressPage(request: Request, pointer: MarkdownPointer, response: Response): Promise<Response> {
301+
async function cacheWordPressPage(request: Request, pointer: MarkdownPointer, response: Response, bucket: R2Bucket): Promise<Response> {
275302
if (!isCacheableWordPressPageRequest(request) || response.status !== 200 || !response.headers.get("content-type")?.includes("text/html") || response.headers.has("set-cookie")) return response
276-
if (request.method === "HEAD") return pageCacheResponse(response, true, "miss")
303+
if (request.method === "HEAD") return pageCacheResponse(response, true, "miss", "render")
277304
const cache = typeof caches === "undefined" ? undefined : (caches as CacheStorage & { default?: Cache }).default
278-
const cacheable = pageCacheResponse(response, false, "miss")
279-
if (cache) {
280-
try {
281-
await cache.put(wordPressPageCacheKey(request, pointer), cacheable.clone())
282-
} catch {
283-
// Page caching is an optimization; canonical rendering remains authoritative.
305+
const cacheable = pageCacheResponse(response, false, "miss", "render")
306+
try {
307+
const snapshot: WordPressPageSnapshot = {
308+
schema: "wp-codebox/wordpress-page/v1",
309+
status: cacheable.status,
310+
statusText: cacheable.statusText,
311+
headers: Array.from(cacheable.headers.entries()),
312+
body: await cacheable.clone().text(),
284313
}
314+
await Promise.all([
315+
cache ? cache.put(wordPressPageCacheKey(request, pointer), cacheable.clone()) : Promise.resolve(),
316+
bucket.put(await wordPressPageSnapshotKey(request, pointer), JSON.stringify(snapshot), { httpMetadata: { contentType: "application/json" } }),
317+
])
318+
} catch {
319+
// Page caching is an optimization; canonical rendering remains authoritative.
285320
}
286321
return cacheable
287322
}
288323

289324
function wordPressPageCacheKey(request: Request, pointer: MarkdownPointer): Request {
290325
const url = new URL(request.url)
291326
url.searchParams.set("__wp_codebox_revision", pointer.revision)
327+
url.searchParams.set("__wp_codebox_page_cache", WORDPRESS_PAGE_CACHE_SCHEMA)
292328
return new Request(url, { method: "GET" })
293329
}
294330

295-
function pageCacheResponse(response: Response, head: boolean, status: "hit" | "miss"): Response {
331+
async function wordPressPageSnapshotKey(request: Request, pointer: MarkdownPointer): Promise<string> {
332+
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(new URL(request.url).toString()))
333+
const hash = Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("")
334+
return `${R2_WORDPRESS_PAGE_PREFIX}/${pointer.revision}/${hash}.json`
335+
}
336+
337+
function pageCacheResponse(response: Response, head: boolean, status: "hit" | "miss", source: "edge" | "r2" | "render"): Response {
296338
const headers = new Headers(response.headers)
297339
headers.set("cache-control", "public, max-age=60, s-maxage=31536000")
298340
headers.set("x-wp-codebox-page-cache", status)
341+
headers.set("x-wp-codebox-page-cache-source", source)
299342
return new Response(head ? null : response.body, { status: response.status, statusText: response.statusText, headers })
300343
}
301344

tests/cloudflare-runtime.test.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -250,10 +250,15 @@ test("Cloudflare keeps PHP-WASM in the entry Worker and uses the Durable Object
250250
assert.match(worker, /"x-wp-codebox-static": "wordpress-archive"/)
251251
assert.match(worker, /cache\.match\(request\)/)
252252
assert.match(worker, /cache\.put\(request, response\.clone\(\)\)/)
253-
assert.match(worker, /matchWordPressPageCache\(request, lease\.pointer\)/)
254-
assert.match(worker, /cacheWordPressPage\(request, lease\.pointer, response\)/)
253+
assert.match(worker, /coordinatorCall<CoordinatorState>\(coordinator, request\.url, "state"\)/)
254+
assert.ok(worker.indexOf('coordinatorCall<CoordinatorState>(coordinator, request.url, "state")') < worker.indexOf("let lease = await acquireLease"))
255+
assert.match(worker, /matchWordPressPageCache\(request, lease\.pointer, env\.WORDPRESS_STATE_BUCKET\)/)
256+
assert.match(worker, /cacheWordPressPage\(request, lease\.pointer, response, env\.WORDPRESS_STATE_BUCKET\)/)
255257
assert.match(worker, /__wp_codebox_revision/)
258+
assert.match(worker, /R2_WORDPRESS_PAGE_PREFIX/)
259+
assert.match(worker, /"wp-codebox\/wordpress-page\/v1"/)
256260
assert.match(worker, /"x-wp-codebox-page-cache"/)
261+
assert.match(worker, /"x-wp-codebox-page-cache-source"/)
257262
assert.match(worker, /"public, max-age=60, s-maxage=31536000"/)
258263
assert.match(materializer, /materializeWordPressRuntimeArtifact\(php, bucket, wordpressRuntimeArtifactManifest/)
259264
assert.doesNotMatch(materializer, /decodeRemoteZip\(WORDPRESS_ARCHIVE_URL/)

0 commit comments

Comments
 (0)