@@ -17,7 +17,7 @@ import {
1717 isBlueprintBundle ,
1818} from '@wp-playground/blueprints' ;
1919import { logger } from '@php-wasm/logger' ;
20- import { setupPostMessageRelay } from '@php-wasm/web' ;
20+ import { type SyncProgress , setupPostMessageRelay } from '@php-wasm/web' ;
2121import { startPlaygroundWeb } from '@wp-playground/client' ;
2222import type { PlaygroundClient } from '@wp-playground/remote' ;
2323import { getRemoteUrl } from '../../config' ;
@@ -27,7 +27,11 @@ import {
2727 setGitHubAuthRepoUrl ,
2828} from './slice-ui' ;
2929import type { PlaygroundDispatch , PlaygroundReduxState } from './store' ;
30- import { selectSiteBySlug } from './slice-sites' ;
30+ import {
31+ isAutosavedSite ,
32+ selectSiteBySlug ,
33+ updateSiteMetadata ,
34+ } from './slice-sites' ;
3135// @ts -ignore
3236import { corsProxyUrl } from 'virtual:cors-proxy-url' ;
3337import { modalSlugs } from './slice-ui' ;
@@ -142,7 +146,10 @@ export function bootSiteClient(
142146 } ,
143147 extraLibraries : site . metadata . runtimeConfiguration
144148 . extraLibraries as any [ ] ,
145- constants : site . metadata . runtimeConfiguration . constants ,
149+ constants : {
150+ ...site . metadata . runtimeConfiguration . constants ,
151+ ...site . metadata . playgroundDefinedConstants ,
152+ } ,
146153 } ;
147154 } else {
148155 blueprint = site . metadata . originalBlueprint ;
@@ -161,6 +168,27 @@ export function bootSiteClient(
161168 ? 'install-from-existing-files-if-needed'
162169 : 'download-and-install' ;
163170
171+ // A newly created saved site has wp-runtime.json in OPFS, but not the
172+ // WordPress tree yet. The first boot must let the setup URL build that
173+ // tree in MEMFS. Passing the OPFS mount here would copy the
174+ // metadata-only directory into /wordpress, so seed OPFS from MEMFS after
175+ // the iframe connects instead.
176+ const shouldFinishInitialOpfsSyncInBackground =
177+ site . metadata . initialOpfsSyncPending === true &&
178+ site . metadata . storage === 'opfs' &&
179+ ! ! mountDescriptor &&
180+ ! isWordPressInstalled ;
181+ const syncOperation = isAutosavedSite ( site ) ? 'autosave' : 'save' ;
182+ const mounts =
183+ mountDescriptor && ! shouldFinishInitialOpfsSyncInBackground
184+ ? [
185+ {
186+ ...mountDescriptor ,
187+ initialSyncDirection : 'opfs-to-memfs' as const ,
188+ } ,
189+ ]
190+ : [ ] ;
191+
164192 let playground : PlaygroundClient | undefined = undefined ;
165193 try {
166194 const phpExtensions = phpExtensionQueryArgsToExtensionsArray (
@@ -187,14 +215,7 @@ export function bootSiteClient(
187215 } ,
188216 // Log Blueprint events
189217 onBlueprintValidated : logBlueprintEvents ,
190- mounts : mountDescriptor
191- ? [
192- {
193- ...mountDescriptor ,
194- initialSyncDirection : 'opfs-to-memfs' ,
195- } ,
196- ]
197- : [ ] ,
218+ mounts,
198219 wordpressInstallMode,
199220 corsProxy : corsProxyUrl ,
200221 gitAdditionalHeadersCallback : createGitAuthHeaders ( ) ,
@@ -290,19 +311,140 @@ export function bootSiteClient(
290311 if ( signal . aborted || ! playground ) {
291312 return ;
292313 }
314+ const connectedPlayground = playground as PlaygroundClient ;
293315
294316 setupPostMessageRelay ( iframe , document . location . origin ) ;
295317
296318 dispatch (
297319 addClientInfo ( {
298320 siteSlug : site . slug ,
299321 url : '/' ,
300- client : playground ,
322+ client : connectedPlayground ,
301323 opfsMountDescriptor : mountDescriptor ,
324+ opfsSync : shouldFinishInitialOpfsSyncInBackground
325+ ? {
326+ status : 'syncing' ,
327+ operation : syncOperation ,
328+ }
329+ : undefined ,
302330 } )
303331 ) ;
332+ if ( site . metadata . storage !== 'none' ) {
333+ try {
334+ await dispatch (
335+ updateSiteMetadata ( {
336+ slug : site . slug ,
337+ changes : {
338+ whenLastUsed : Date . now ( ) ,
339+ } ,
340+ } )
341+ ) ;
342+ } catch ( error ) {
343+ logger . error ( 'Error updating Playground last-used time' , error ) ;
344+ }
345+ }
346+
347+ // If WordPress was already present in OPFS, the regular OPFS-to-MEMFS
348+ // mount above completed the initial sync path.
349+ const shouldClearInitialOpfsSyncPending =
350+ site . metadata . initialOpfsSyncPending === true &&
351+ site . metadata . storage === 'opfs' &&
352+ ! ! mountDescriptor &&
353+ ! shouldFinishInitialOpfsSyncInBackground ;
354+ if ( shouldClearInitialOpfsSyncPending ) {
355+ try {
356+ await dispatch (
357+ updateSiteMetadata ( {
358+ slug : site . slug ,
359+ changes : {
360+ initialOpfsSyncPending : false ,
361+ } ,
362+ } )
363+ ) ;
364+ } catch ( error ) {
365+ logger . error (
366+ 'Error updating Playground initial sync state' ,
367+ error
368+ ) ;
369+ }
370+ }
371+
372+ if ( shouldFinishInitialOpfsSyncInBackground && mountDescriptor ) {
373+ // Progress callbacks cross worker and iframe boundaries asynchronously.
374+ // A final progress message may arrive after mountOpfs() resolves and
375+ // must not resurrect the completed sync state in Redux.
376+ let opfsMountSettled = false ;
377+ void connectedPlayground
378+ . mountOpfs (
379+ {
380+ ...mountDescriptor ,
381+ initialSyncDirection : 'memfs-to-opfs' ,
382+ } ,
383+ ( progress : SyncProgress ) => {
384+ if ( opfsMountSettled ) {
385+ return ;
386+ }
387+ dispatch (
388+ updateClientInfo ( {
389+ siteSlug : site . slug ,
390+ changes : {
391+ opfsSync : {
392+ status : 'syncing' ,
393+ progress,
394+ operation : syncOperation ,
395+ } ,
396+ } ,
397+ } )
398+ ) ;
399+ }
400+ )
401+ . then ( async ( ) => {
402+ opfsMountSettled = true ;
403+ try {
404+ await dispatch (
405+ updateSiteMetadata ( {
406+ slug : site . slug ,
407+ changes : {
408+ initialOpfsSyncPending : false ,
409+ } ,
410+ } )
411+ ) ;
412+ } catch ( error ) {
413+ logger . error (
414+ 'Error updating Playground initial sync state' ,
415+ error
416+ ) ;
417+ }
418+ dispatch (
419+ updateClientInfo ( {
420+ siteSlug : site . slug ,
421+ changes : {
422+ opfsSync : undefined ,
423+ } ,
424+ } )
425+ ) ;
426+ } )
427+ . catch ( ( error : unknown ) => {
428+ opfsMountSettled = true ;
429+ logger . error (
430+ 'Error syncing saved Playground to OPFS' ,
431+ error
432+ ) ;
433+ dispatch (
434+ updateClientInfo ( {
435+ siteSlug : site . slug ,
436+ changes : {
437+ opfsSync : {
438+ status : 'error' ,
439+ operation : syncOperation ,
440+ } ,
441+ } ,
442+ } )
443+ ) ;
444+ } ) ;
445+ }
304446
305- ( playground as PlaygroundClient ) . onNavigation ( ( url ) => {
447+ connectedPlayground . onNavigation ( ( url ) => {
306448 dispatch (
307449 updateClientInfo ( {
308450 siteSlug : site . slug ,
0 commit comments