@@ -13,11 +13,26 @@ const WORDPRESS_ARCHIVE_URL = "https://wordpress.org/latest.zip"
1313const SQLITE_INTEGRATION_ARCHIVE_URL = "https://github.com/WordPress/sqlite-database-integration/releases/download/v2.2.23/plugin-sqlite-database-integration.zip"
1414const SITE_URL = "https://wp-codebox-runtime.invalid"
1515const 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"
1618let bootPromise : Promise < { php : PHP ; wordpressVersion : string } > | undefined
1719
20+ interface Env {
21+ WORDPRESS_STATE : DurableObjectNamespace
22+ WORDPRESS_STATE_BUCKET : R2Bucket
23+ }
24+
1825export 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+
41135async function runBootProbe ( phase : string ) : Promise < Response > {
42136 if ( phase === "wordpress-archive" || phase === "sqlite-archive" ) {
43137 const archive = phase === "wordpress-archive"
0 commit comments