EditorAssetBundle.copy(to:) commits a downloaded asset bundle by deleting the destination and copying into it. Two editors on the same site can run that concurrently, so one can delete the directory the other is currently serving assets from.
Detail
ios/Sources/GutenbergKit/Sources/Model/EditorAssetBundle.swift:217-223 is directoryExists → removeItem → createDirectory → copyItem, against storageRoot/<manifest-checksum> (EditorAssetLibrary.swift:245).
Nothing serializes two writers. Each EditorViewController builds its own EditorService (EditorViewController.swift:223), which builds its own EditorAssetLibrary — actor isolation is per-instance, so two editors for the same site are two independent actors writing the same path.
How it bites
Cold asset cache (first open of a site, or after deleteAllData), slow connection. The user opens the editor, backs out, and immediately reopens.
Editor B downloads and commits, then starts serving assets from storageRoot/<checksum>. Editor A — still finishing its own download — reaches copy(to:), sees the directory exists, removes it, and re-copies. For the duration of that window every plugin/theme asset request misses the bundle.
The blast radius is smaller than it first looks, and worth stating so nobody over-fixes it:
- The path is checksum-keyed, so identical checksum means identical content. This is a window of absence, not corruption.
EditorAssetBundleProvider.swift:78-85 already falls back to fetchFromRemote for any asset missing from the bundle, so the visible editor degrades to network refetches rather than erroring. Offline, those assets just don't load.
prepareAssetBundle() short-circuits on readAssetBundles().first (EditorService.swift:234-243), so a warm cache never reaches copy(to:) at all.
Suggested fix
Commit via an atomic rename — write to a sibling temp directory and replaceItemAt — or skip the copy entirely when hasBundle(forManifestChecksum:) is already true.
Found while reviewing #651. Pre-existing; the removal of the mid-load cancellation widens the window slightly, but the old cancellation was worse here (see #667).
EditorAssetBundle.copy(to:)commits a downloaded asset bundle by deleting the destination and copying into it. Two editors on the same site can run that concurrently, so one can delete the directory the other is currently serving assets from.Detail
ios/Sources/GutenbergKit/Sources/Model/EditorAssetBundle.swift:217-223isdirectoryExists→removeItem→createDirectory→copyItem, againststorageRoot/<manifest-checksum>(EditorAssetLibrary.swift:245).Nothing serializes two writers. Each
EditorViewControllerbuilds its ownEditorService(EditorViewController.swift:223), which builds its ownEditorAssetLibrary— actor isolation is per-instance, so two editors for the same site are two independent actors writing the same path.How it bites
Cold asset cache (first open of a site, or after
deleteAllData), slow connection. The user opens the editor, backs out, and immediately reopens.Editor B downloads and commits, then starts serving assets from
storageRoot/<checksum>. Editor A — still finishing its own download — reachescopy(to:), sees the directory exists, removes it, and re-copies. For the duration of that window every plugin/theme asset request misses the bundle.The blast radius is smaller than it first looks, and worth stating so nobody over-fixes it:
EditorAssetBundleProvider.swift:78-85already falls back tofetchFromRemotefor any asset missing from the bundle, so the visible editor degrades to network refetches rather than erroring. Offline, those assets just don't load.prepareAssetBundle()short-circuits onreadAssetBundles().first(EditorService.swift:234-243), so a warm cache never reachescopy(to:)at all.Suggested fix
Commit via an atomic rename — write to a sibling temp directory and
replaceItemAt— or skip the copy entirely whenhasBundle(forManifestChecksum:)is already true.Found while reviewing #651. Pre-existing; the removal of the mid-load cancellation widens the window slightly, but the old cancellation was worse here (see #667).