Overview
ThumbnailCache provides a two-tier caching system for R2 object thumbnails. It combines in-memory caching with disk persistence and implements request coalescing to prevent duplicate fetches. The actor-based design ensures thread-safe access from any context.
Located at: Fiaxe/Services/ThumbnailCache.swift:6
Type Definition
Implemented as an actor for thread-safe concurrent access. All methods can be called from any isolation context.
Singleton Instance
static let shared = ThumbnailCache ()
Access the shared cache instance.
Methods
thumbnail()
Retrieves or generates a thumbnail for an R2 object key.
func thumbnail ( for key : String , credentials : R2Credentials) async -> NSImage ?
The R2 object key to generate a thumbnail for (e.g., "photos/image.jpg").
R2 credentials used to generate presigned URLs for fetching the image.
Returns: Thumbnail image (120×120 max), or nil if generation fails.
Implementation Details
// Example from ThumbnailCache.swift:30-64
func thumbnail ( for key : String , credentials : R2Credentials) async -> NSImage ? {
// Scope cache key by bucket so different buckets never share thumbnails
let scopedKey = " \( credentials. bucketName ) / \( key ) "
let cacheKey = scopedKey as NSString
// 1. Memory cache hit
if let img = memoryCache. object ( forKey : cacheKey) { return img }
// 2. Disk cache hit
if let img = loadFromDisk ( key : scopedKey) {
memoryCache. setObject (img, forKey : cacheKey)
return img
}
// 3. Coalesce in-flight requests
if let task = inFlight[scopedKey] { return await task. value }
// Generate presigned URL — works whether or not a custom domain is set
guard let url = AWSV4Signer. presignedURL ( for : key, credentials : credentials) else {
return nil
}
let task = Task < NSImage ? , Never > {
let img = await fetchThumbnail ( url : url, key : key)
if let img {
memoryCache. setObject (img, forKey : cacheKey)
saveToDisk (img, key : scopedKey)
}
return img
}
inFlight[scopedKey] = task
let result = await task. value
inFlight. removeValue ( forKey : scopedKey)
return result
}
The method implements a three-tier lookup:
Memory cache - Instant return if cached in NSCache
Disk cache - Load from disk if available, promote to memory
Network fetch - Generate thumbnail from R2, cache in both memory and disk
Request Coalescing If multiple requests for the same thumbnail arrive simultaneously, only one network request is made. Other requests await the same task result.
clearMemory()
Clears the entire in-memory cache.
Useful for freeing memory during low-memory conditions. Disk cache is preserved.
Cache Configuration
The cache is configured with size limits:
private init () {
memoryCache. countLimit = 500 // Max 500 images
memoryCache. totalCostLimit = 100 * 1024 * 1024 // Max 100 MB
}
Maximum number of images in memory cache (500).
Maximum memory footprint in bytes (100 MB).
NSCache automatically evicts least-recently-used items when limits are exceeded.
Disk Cache
Thumbnails are persisted to the user’s cache directory:
private let diskCacheURL: URL = {
let caches = FileManager. default . urls ( for : . cachesDirectory , in : . userDomainMask ). first !
let dir = caches. appendingPathComponent ( "R2VaultThumbnails" , isDirectory : true )
try ? FileManager. default . createDirectory ( at : dir, withIntermediateDirectories : true )
return dir
}()
Disk cache location:
~/Library/Caches/R2VaultThumbnails/
File Naming
Cache keys are sanitized for safe filenames:
private func diskURL ( for key : String ) -> URL {
// Use a safe filename derived from the key
let safe = key
. replacingOccurrences ( of : "/" , with : "_" )
. replacingOccurrences ( of : ":" , with : "_" )
return diskCacheURL. appendingPathComponent (safe + ".png" )
}
Examples:
photos/vacation.jpg → photos_vacation.jpg.png
bucket:folder/file.png → bucket_folder_file.png.png
All thumbnails are stored as PNG:
private func saveToDisk ( _ image : NSImage, key : String ) {
guard let tiff = image.tiffRepresentation,
let rep = NSBitmapImageRep ( data : tiff),
let png = rep. representation ( using : . png , properties : [ : ])
else { return }
try ? png. write ( to : diskURL ( for : key))
}
PNG format provides lossless compression suitable for thumbnails.
Thumbnail Generation
Image Thumbnails
Generated by downloading and resizing:
private func imageThumbnail ( url : URL) async -> NSImage ? {
guard let (data, _ ) = try ? await URLSession.shared. data ( from : url),
let src = NSImage ( data : data)
else { return nil }
return resized (src, to : CGSize ( width : 120 , height : 120 ))
}
Video Thumbnails
Generated using AVFoundation:
private func videoThumbnail ( url : URL) async -> NSImage ? {
let asset = AVURLAsset ( url : url)
let gen = AVAssetImageGenerator ( asset : asset)
gen. appliesPreferredTrackTransform = true
gen. maximumSize = CGSize ( width : 120 , height : 120 )
guard let cgImage = try ? await gen. image ( at : . zero ). image
else { return nil }
return NSImage ( cgImage : cgImage, size : NSSize ( width : 120 , height : 120 ))
}
Extracts the first frame from video files.
Supported Images JPEG, PNG, GIF, HEIC, BMP, TIFF, WebP
Supported Videos MP4, MOV, AVI, MKV, WebM, M4V
Resize Algorithm
The cache maintains aspect ratio while fitting within 120×120:
private func resized ( _ image : NSImage, to size : CGSize) -> NSImage {
let original = image. size
guard original.width > 0 , original.height > 0 else { return image }
let scale = min (size. width / original. width , size. height / original. height )
let newSize = CGSize ( width : original. width * scale, height : original. height * scale)
let result = NSImage ( size : newSize)
result. lockFocus ()
image. draw ( in : CGRect ( origin : . zero , size : newSize),
from : CGRect ( origin : . zero , size : original),
operation : . copy , fraction : 1 )
result. unlockFocus ()
return result
}
Examples:
1920×1080 → 120×67.5 (fits width)
800×1200 → 80×120 (fits height)
100×100 → 100×100 (no upscaling)
Request Coalescing
Prevents duplicate fetches for the same thumbnail:
private var inFlight: [ String : Task<NSImage ? , Never >] = [ : ]
// 3. Coalesce in-flight requests
if let task = inFlight[scopedKey] { return await task. value }
let task = Task < NSImage ? , Never > {
let img = await fetchThumbnail ( url : url, key : key)
// ... cache and return
}
inFlight[scopedKey] = task
let result = await task. value
inFlight. removeValue ( forKey : scopedKey)
If 10 views request the same thumbnail simultaneously:
Only 1 network request is made
All 10 requests await the same task
All receive the same result
Bucket Scoping
Cache keys are scoped by bucket name:
let scopedKey = " \( credentials. bucketName ) / \( key ) "
This ensures:
Different buckets with same key names don’t share thumbnails
Switching between buckets doesn’t show wrong thumbnails
Cache hits are always bucket-specific
Usage Example
import SwiftUI
struct ThumbnailView : View {
let object: R2Object
let credentials: R2Credentials
@State private var thumbnail: NSImage ?
var body: some View {
Group {
if let thumbnail {
Image ( nsImage : thumbnail)
. resizable ()
. aspectRatio ( contentMode : . fit )
. frame ( width : 120 , height : 120 )
} else {
ProgressView ()
. frame ( width : 120 , height : 120 )
}
}
. task {
// Automatically fetches thumbnail
thumbnail = await ThumbnailCache. shared . thumbnail (
for : object. key ,
credentials : credentials
)
}
}
}
NSCache automatically evicts items under memory pressure. The 100 MB limit prevents excessive memory usage.
PNG compression reduces disk space. A 120×120 thumbnail typically uses 10-50 KB.
Actor isolation ensures safe concurrent access. Multiple views can request thumbnails simultaneously without race conditions.
Request coalescing prevents duplicate downloads. Once cached, thumbnails load instantly.
Cache Persistence
Disk Cache Lifetime The disk cache persists across app launches and remains until:
User manually clears cache (if implemented)
macOS automatically clears ~/Library/Caches during cleanup
App is uninstalled
Memory cache is cleared on every app launch.
Error Handling
The cache gracefully handles errors:
func thumbnail ( for key : String , credentials : R2Credentials) async -> NSImage ? {
// Returns nil on failure
}
Failure cases:
Invalid presigned URL
Network errors
Unsupported file formats
Corrupted images
Disk write failures
All failures return nil rather than throwing errors, allowing UI to show fallback icons.
Thread Safety
Actor Isolation The actor keyword ensures all cache operations are serialized:
No race conditions on inFlight dictionary
Safe NSCache access (though NSCache is already thread-safe)
Predictable behavior under concurrent load
// Safe from any context
Task. detached {
let thumb = await ThumbnailCache. shared . thumbnail ( for : key, credentials : creds)
}