Skip to content

[Website] Delay .zip Playground imports until OPFS sync is finished - #3847

Merged
adamziel merged 1 commit into
trunkfrom
codex/fix-concurrent-zip-import
Jul 1, 2026
Merged

[Website] Delay .zip Playground imports until OPFS sync is finished#3847
adamziel merged 1 commit into
trunkfrom
codex/fix-concurrent-zip-import

Conversation

@ashfame

@ashfame ashfame commented Jun 30, 2026

Copy link
Copy Markdown
Member

What it does

Before this PR, importing a Playground .zip could start while the newly-created saved Playground was still autosaving to OPFS.

After this PR, the import waits until OPFS sync finishes. This avoids modifying MEMFS file handles that are already targeted for OPFS synchronization.

Rationale

Importing a .zip file to restore a previous Playground was failing when started while the initial autosave was still running.

Testing instructions

  1. Create a Playground
  2. Download it as a zip file
  3. Close the Playground browser tab and reopen it
  4. Import the downloaded zip file
  5. Confirm the zip imports cleanly
@ashfame
ashfame force-pushed the codex/fix-concurrent-zip-import branch from 50bc8ee to 93a0e7f Compare June 30, 2026 19:09
@ashfame
ashfame marked this pull request as ready for review June 30, 2026 19:58
@ashfame
ashfame requested review from a team and zaerl June 30, 2026 19:58
}
};
doImport();
}, [pendingZipFile, pendingZipTargetSlug, activeSite, playground, onClose]);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if there is a deeper cause we ought to be fixing here. We are already using a useEffect() hook to avoid duplicating import activity.

Why isn't it working? What is now changing to restart the import?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm finding that the onClose handler keeps changing. If I remove that, from the effect dependency list, the apparent problem disappears and, from a user perspective, the import appears to succeed.

It seems like we we shouldn't base our effect on the value of a provided onClose prop, but I bet there is a reason it was added.

One thing to note:
Even though the import appears to succeed after removing onClose from the effect deps, there is the following error. I'm not sure yet whether it is related to removing onClose or another issue.

Error syncing saved Playground to OPFS NotFoundError: A requested file or directory could not be found at the time an operation was processed.
    at newError (serialize-error.ts:115:5)
    at Module.deserializeError (serialize-error.ts:274:8)
    at Object.deserialize (api.ts:657:34)
    at fromWireValue (comlink-sync.ts:958:45)
    at async Object.mountOpfs (boot-playground-remote.ts:393:11)Caused by: Error: Comlink method call failed
    at Object.deserialize (api.ts:668:32)
    at fromWireValue (comlink-sync.ts:958:45)
    at async Object.mountOpfs (boot-playground-remote.ts:393:11)Caused by: Error: Comlink method call failed
    at Object.deserialize (api.ts:668:32)
    at fromWireValue (comlink-sync.ts:958:45)
    at async syncInitialOpfsFilesInBackground (boot-site-client.ts:423:3)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To clarify, just removing onClose from the dependency list on trunk, without this PR, seems to stop the duplicate importing.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Codex suggested that Autosaving new Playgrounds causes this problem, sounds somewhat plausible:

The failure is a race in the browser “Import .zip” flow:

  • Import creates a new OPFS saved site.
  • That new site first boots in MEMFS, then copies initial WordPress files to OPFS in the background.
  • The import can start mutating /wordpress while that initial OPFS copy is still running.
  • This can leave the site showing “Save failed” or persisted with stale/missing files.

@brandonpayton brandonpayton Jul 1, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The OPFS NotFoundError doesn't happen with this PR's changes, but it happens when I just remove the onClose dependency from the import effect.

The failure is a race in the browser “Import .zip” flow:

That appears to be the case. Importing a zip calls createSiteForImport().

const handleImportZip = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
try {
const targetSlug = await createSiteForImport();
setPendingZipTargetSlug(targetSlug);
setPendingZipFile(file);
} catch (error) {

And createSiteForImport() is creates a new OPFS-backed site:

/**
* Creates or selects a target Playground before importing a zip archive.
*
* Imports prefer a new OPFS-backed site so the result survives a refresh.
* If that cannot be created, the import falls back to an existing or new
* temporary site.
*/
async function createSiteForImport() {
try {
return await sitesAPI.createNewSavedSite();
} catch {

I believe this PR doesn't experience the OPFS NotFoundError because it delays the import while activeOpfsSyncStatus is "syncing". I think that is probably the right call. We could be even more explicit about setting an "importing" state or about a new site being completely ready for use, but I think this PR's current approach is a reasonable touch and maybe the right depth for now.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note: I edited the above comment to make more sense.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@brandonpayton I don't like when Codex amends commits, otherwise you could have seen the iterations that you went through yourself :) I will do a follow up PR today addressing your feedback.

@brandonpayton brandonpayton left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for working on this fix, @ashfame. It is testing well for me using the original failed import zip and an import zip I created locally.

I think this is the right touch for such a fix. Nice job!

I left one review suggestion that would be cool to address. Overall, I think this is good to go.

wordPressFilesZip: pendingZipFile,
wordPressFilesZip: zipFile,
});
await flushImportedWordPressFiles(playground);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be helpful to add an inline comment about why we are flushing, since we were not flushing before this PR.

It seems like a reasonable thing to do though.

@brandonpayton

Copy link
Copy Markdown
Member

@ashfame, I forgot to mention as part of the review:
It would be great to add an E2E test for this, but it could be done as a follow up PR.

@adamziel adamziel changed the title [codex] Prevent duplicate Playground zip imports Jul 1, 2026
@adamziel
adamziel merged commit 7577913 into trunk Jul 1, 2026
55 checks passed
@adamziel
adamziel deleted the codex/fix-concurrent-zip-import branch July 1, 2026 10:14
brandonpayton added a commit that referenced this pull request Jul 18, 2026
## What changed

- Add an inline comment explaining why ZIP imports explicitly flush OPFS
before reporting success.
- Add Chromium E2E coverage for imported saved-site durability.

## E2E test coverage

1. Creates a temporary Playground with a known marker and saves it to
OPFS.
2. Imports a generated ZIP containing a distinct marker into a new saved
Playground.
3. Waits for exactly one success alert and resolves the imported site
slug.
4. Immediately fresh-boots the imported slug from `?site-slug=<slug>`,
discarding the runtime that performed the import.
5. Requests the imported marker from the fresh runtime and confirms that
it was restored from saved storage.
6. Verifies that no “Save failed” or “Site failed” UI appears.
7. Switches to the original saved site, then back to the imported site,
and verifies that the marker still loads.
8. Direct-loads the imported slug once more and verifies that the marker
remains available.

## Why

The previous assertion order requested the imported marker from the
active import runtime before switching sites. Completing that request
emits `request.end`, which can trigger background OPFS flushing and mask
files that were not durable when import success was reported.

Fresh-booting first checks the user-visible contract: once import
reports success, the imported files can be restored from saved storage.
This intentionally remains a black-box durability check; it does not
couple the test to Comlink or assert which OPFS flush path completed.

Follow-up to review feedback on #3847.

---------

Co-authored-by: Brandon Payton <brandon@happycode.net>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment