@@ -42,11 +42,14 @@ actor EditorService {
4242 self . siteURL = siteURL
4343 self . urlSession = urlSession
4444
45- self . storeURL = URL . applicationDirectory
46- . appendingPathComponent ( " GutenbergKit " , isDirectory: true )
45+ self . storeURL = EditorService . rootURL
4746 . appendingPathComponent ( siteURL. sha1, isDirectory: true )
4847 }
4948
49+ private static var rootURL : URL {
50+ URL . applicationDirectory. appendingPathComponent ( " GutenbergKit " , isDirectory: true )
51+ }
52+
5053 /// Set up the editor for the given site.
5154 ///
5255 /// - warning: The request make take a significant amount of time the first
@@ -194,43 +197,13 @@ actor EditorService {
194197 /// Returns the editor assets manifest as a JSON string, with JavaScript and stylesheet links
195198 /// modified so that their content can be cached and reused by the editor.
196199 ///
197- /// Verifies that all required assets are cached before returning the manifest.
198- ///
199200 /// - Parameter siteURL: The site URL to extract the scheme for scheme-less links
200201 /// - Returns: JSON string of the processed manifest
201- /// - Throws: `EditorServiceError` if assets are missing or manifest processing fails
202202 private func getManifestForEditor( siteURL: String ) throws -> String {
203203 // For scheme-less links (i.e. '//stats.wp.com/w.js'), use the scheme in `siteURL`.
204204 let siteURLScheme = URL ( string: siteURL) ? . scheme
205205 let data = try Data ( contentsOf: manifestFileURL)
206206 let manifest = try JSONDecoder ( ) . decode ( EditorAssetsManifest . self, from: data)
207- let assetLinks = try manifest. parseAssetLinks ( )
208-
209- // Verify all assets are cached
210- let fileManager = FileManager . default
211- var missingAssets : [ String ] = [ ]
212-
213- for urlString in assetLinks {
214- let filename = cachedFilename ( for: urlString)
215- let localURL = assetsDirectoryURL. appendingPathComponent ( filename)
216-
217- if !fileManager. fileExists ( atPath: localURL. path) {
218- missingAssets. append ( urlString)
219- }
220- }
221-
222- if !missingAssets. isEmpty {
223- log ( . error, " Missing \( missingAssets. count) asset(s) from cache " )
224- for (index, asset) in missingAssets. prefix ( 5 ) . enumerated ( ) {
225- log ( . error, " [ \( index + 1 ) ] \( asset) " )
226- }
227- if missingAssets. count > 5 {
228- log ( . error, " ... and \( missingAssets. count - 5 ) more " )
229- }
230- throw URLError ( . resourceUnavailable)
231- }
232-
233- log ( . info, " All \( assetLinks. count) manifest assets verified in cache " )
234207
235208 // Process manifest for editor
236209 let processedData = try manifest. renderForEditor ( defaultScheme: siteURLScheme)
@@ -244,8 +217,10 @@ actor EditorService {
244217
245218 /// Fetches all assets from the manifest and stores them on the device
246219 private func fetchAssets( manifestData: Data ) async throws {
220+ let startTime = CFAbsoluteTimeGetCurrent ( )
247221 let manifest = try JSONDecoder ( ) . decode ( EditorAssetsManifest . self, from: manifestData)
248222 let assetLinks = try manifest. parseAssetLinks ( )
223+ . filter { isSupportedAsset ( $0) }
249224
250225 log ( . info, " Found \( assetLinks. count) assets to fetch " )
251226
@@ -258,71 +233,81 @@ actor EditorService {
258233 // Track statistics
259234 var fetchedCount = 0
260235 var cachedCount = 0
261- var totalSize : Int64 = 0
236+ var assetURLs : [ URL ] = [ ]
262237
263238 // Fetch all assets in parallel
264- try await withThrowingTaskGroup ( of: ( Bool, Int64 ) . self) { group in
239+ await withTaskGroup ( of: Result < ( Bool , URL ) , Error > . self) { group in
265240 for link in assetLinks {
266241 group. addTask {
267- try await self . fetchAsset ( from: link)
242+ await Result { try await self . fetchAsset ( from: link) }
268243 }
269244 }
270245
271- for try await (wasCached, size) in group {
272- if wasCached {
273- cachedCount += 1
274- } else {
275- fetchedCount += 1
246+ for await result in group {
247+ switch result {
248+ case . success( let ( wasCached, url) ) :
249+ if wasCached {
250+ cachedCount += 1
251+ } else {
252+ fetchedCount += 1
253+ }
254+ assetURLs. append ( url)
255+ case . failure( let error) :
256+ log ( . error, " Failed to fetch asset: \( error) " )
276257 }
277- totalSize += size
278258 }
279259 }
280260
281- let totalSizeMB = Double ( totalSize ) / ( 1024 * 1024 )
282- log ( . info, " Assets loaded: \( fetchedCount) fetched, \( cachedCount) cached, total size: \( String ( format: " %.2f " , totalSizeMB ) ) MB " )
261+ let totalTime = CFAbsoluteTimeGetCurrent ( ) - startTime
262+ log ( . info, " Assets loaded: \( fetchedCount) fetched, \( cachedCount) cached, total size: \( assetURLs . reduce ( 0 ) { $0 + $1 . fileSize } . formatted ) in \( String ( format: " %.2f " , totalTime ) ) s " )
283263 }
284264
285- /// Fetches a single asset and stores it on disk
286- /// - Returns: A tuple indicating (wasCached, fileSize)
287- private func fetchAsset( from urlString: String ) async throws -> ( Bool , Int64 ) {
265+ /// Checks if an asset URL is supported
266+ private func isSupportedAsset( _ urlString: String ) -> Bool {
288267 guard let url = URL ( string: urlString) else {
289268 log ( . warn, " Malformed asset link: \( urlString) " )
290- return ( false , 0 )
269+ return false
291270 }
292271
293272 guard url. scheme == " http " || url. scheme == " https " else {
294273 log ( . warn, " Unexpected asset link: \( urlString) " )
295- return ( false , 0 )
274+ return false
296275 }
297276
298277 let supportedResourceSuffixes = [ " .js " , " .css " , " .js.map " ]
299278 guard supportedResourceSuffixes. contains ( where: { url. lastPathComponent. hasSuffix ( $0) } ) else {
300279 log ( . warn, " Unsupported asset URL: \( url) " )
301- return ( false , 0 )
280+ return false
281+ }
282+
283+ return true
284+ }
285+
286+ /// Fetches a single asset and stores it on disk
287+ /// - Returns: A tuple indicating (wasCached, fileURL)
288+ private func fetchAsset( from urlString: String ) async throws -> ( Bool , URL ) {
289+ guard let url = URL ( string: urlString) else {
290+ throw URLError ( . badURL)
302291 }
303292
304293 let localURL = assetsDirectoryURL. appendingPathComponent ( cachedFilename ( for: urlString) )
305294
306- // Check if already cached
307295 if FileManager . default. fileExists ( atPath: localURL. path) {
308- let size = try ? FileManager . default. attributesOfItem ( atPath: localURL. path) [ . size] as? Int64 ?? 0
309- return ( true , size ?? 0 )
296+ return ( true , localURL)
310297 }
311298
299+ let startTime = CFAbsoluteTimeGetCurrent ( )
312300 let ( downloadedURL, response) = try await urlSession. download ( from: url)
313- if let status = ( response as? HTTPURLResponse ) ? . statusCode, ( 200 ..< 300 ) . contains ( status) {
314- let size = try ? FileManager . default. attributesOfItem ( atPath: downloadedURL. path) [ . size] as? Int64 ?? 0
315- do {
316- try FileManager . default. moveItem ( at: downloadedURL, to: localURL)
317- } catch {
318- log ( . error, " Failed to move downloaded assets \( downloadedURL) \( localURL) " )
319- }
320- log ( . debug, " Downloaded asset: \( url. lastPathComponent) ( \( size ?? 0 ) bytes) " )
321- return ( false , size ?? 0 )
322- } else {
323- log ( . error, " Received unexpected HTTP response for URL: \( url) " )
324- return ( false , 0 )
301+ let downloadTime = CFAbsoluteTimeGetCurrent ( ) - startTime
302+
303+ guard let status = ( response as? HTTPURLResponse ) ? . statusCode, ( 200 ..< 300 ) . contains ( status) else {
304+ throw URLError ( . badServerResponse)
325305 }
306+
307+ try FileManager . default. moveItem ( at: downloadedURL, to: localURL)
308+
309+ log ( . debug, " Downloaded asset: \( url. lastPathComponent) ( \( localURL. fileSize. formatted) ) in \( String ( format: " %.2f " , downloadTime) ) s " )
310+ return ( false , localURL)
326311 }
327312
328313 /// Loads a cached asset from disk
@@ -347,11 +332,9 @@ actor EditorService {
347332
348333 /// Deletes all cached editor data for all sites
349334 static func deleteAllData( ) throws {
350- let rootURL = URL . documentsDirectory. appendingPathComponent ( " GutenbergKit " , isDirectory: true )
351- guard FileManager . default. fileExists ( atPath: rootURL. path) else {
352- return
335+ if FileManager . default. fileExists ( atPath: EditorService . rootURL. path ( ) ) {
336+ try FileManager . default. removeItem ( at: EditorService . rootURL)
353337 }
354- try FileManager . default. removeItem ( at: rootURL)
355338 }
356339
357340 /// Generates a cached filename from an asset URL using SHA256 hash
@@ -393,3 +376,15 @@ private extension Result {
393376 }
394377 }
395378}
379+
380+ private extension URL {
381+ var fileSize : Int64 {
382+ ( try ? FileManager . default. attributesOfItem ( atPath: path ( ) ) [ . size] as? Int64 ) ?? 0
383+ }
384+ }
385+
386+ private extension Int64 {
387+ var formatted : String {
388+ ByteCountFormatter . string ( fromByteCount: self , countStyle: . file)
389+ }
390+ }
0 commit comments