StorageDelegate
open class StorageDelegate<T> : Storage, @unchecked Sendable where T : Decodable, T : Encodable, T : Sendable
A storage delegate class that provides flexible caching strategies for storage operations.
StorageDelegate acts as a wrapper around any Storage implementation, adding an optional
in-memory caching layer. This enables various performance and resilience patterns depending on
your application’s requirements.
Overview
The delegate pattern allows you to:
- Add caching to any storage implementation without modifying the underlying storage
- Choose from multiple caching strategies to match your use case
- Maintain thread-safe concurrent access through Swift actors
- Provide degraded functionality during storage failures
Caching Strategies
Three caching strategies are available through the CacheStrategy enum:
NO_CACHE (Default)
All operations go directly to the underlying storage with no caching layer.
let storage = StorageDelegate(
delegate: KeychainStorage(),
cacheStrategy: .NO_CACHE
)
Use when: You always need fresh data and memory usage is a concern.
CACHE
Items are cached in memory on save. Reads are served from cache when available.
let storage = StorageDelegate(
delegate: RemoteStorage(),
cacheStrategy: .CACHE
)
Use when: Performance is critical and you can tolerate cache-storage inconsistencies on failures.
CACHE_ON_FAILURE
Items are cached after successful operations. Cache serves as fallback during storage failures.
let storage = StorageDelegate(
delegate: NetworkStorage(),
cacheStrategy: .CACHE_ON_FAILURE
)
Use when: You need resilience against intermittent failures and can tolerate stale data.
Thread Safety
All caching operations are thread-safe through the use of Swift actors. Multiple concurrent reads and writes are handled safely without the need for external synchronization.
Example Usage
// High-performance cached configuration
let config = StorageDelegate(
delegate: KeychainStorage(account: "app.config"),
cacheStrategy: .CACHE
)
try await config.save(item: appConfig)
let cachedConfig = try await config.get() // Served from cache
// Resilient network storage
let userData = StorageDelegate(
delegate: NetworkKeychain(account: "user.data"),
cacheStrategy: .CACHE_ON_FAILURE
)
try await userData.save(item: user)
// Later, even if network fails...
let user = try await userData.get() // Falls back to cache
Note
This class is designed to be subclassed by specific storage strategies (e.g., MemoryStorage,
KeychainStorage) that conform to the Storage protocol.
See also
Parameters
T
|
The type of the object being stored. Must conform to |
-
Initializes a new StorageDelegate with the specified cache strategy.
This is the preferred initializer for creating storage delegates with caching support. The cache strategy determines how the in-memory cache interacts with the underlying storage.
Cache Strategy Behavior
- NO_CACHE: No caching layer is used. All operations go directly to the delegate storage.
- CACHE: Items are cached on save. Subsequent reads are served from cache, falling back to storage only if cache is empty.
- CACHE_ON_FAILURE: Items are cached after successful operations. Cache serves as a fallback when storage operations fail, providing resilience.
Example
// Create a resilient network storage with cache fallback let networkStorage = StorageDelegate( delegate: KeychainStorage(account: "user.token"), cacheStrategy: .CACHE_ON_FAILURE ) // Save succeeds and caches the token try await networkStorage.save(item: authToken) // If keychain becomes unavailable, cache provides fallback let token = try await networkStorage.get() // Returns cached tokenSee also
Declaration
Swift
public init(delegate: any Storage<T>, cacheStrategy: CacheStrategy = .NO_CACHE)Parameters
delegateThe underlying storage to delegate operations to. This can be any type conforming to
Storage<T>, such asMemory<T>,Keychain<T>, or custom implementations.cacheStrategyThe caching strategy to use. Defaults to
.NO_CACHEfor backward compatibility and minimal memory footprint. -
save(item:Asynchronous) Saves the given item to storage with cache behavior determined by the cache strategy.
The save operation behavior varies based on the configured
CacheStrategy:Cache Strategy Behavior
NO_CACHE
The item is saved directly to the underlying storage with no caching.
try await storage.save(item: user) // Item saved to storage onlyCACHE
The item is cached in memory first, then saved to storage. If the storage save fails, the item remains in cache, allowing subsequent reads to return the cached value.
try await storage.save(item: user) // Item cached immediately, then saved to storage // If storage fails, cache still contains the itemCACHE_ON_FAILURE
Attempts to save to storage first. On success, any existing cache is cleared to ensure fresh reads. On failure, the item is cached to provide a fallback for future operations.
try await storage.save(item: user) // On success: Item saved, cache cleared (prefers fresh data) // On failure: Item cached, error thrown (provides fallback)Thread Safety
This method is thread-safe and can be called concurrently from multiple tasks. The underlying cache operations are protected by Swift actors.
Error Handling
Errors from the underlying storage are propagated to the caller. The cache state after an error depends on the cache strategy (see above).
Throws
Any error thrown by the underlying storage implementation.
See also
Declaration
Swift
public func save(item: T) async throwsParameters
itemThe item to save. Must conform to
CodableandSendable. -
get()AsynchronousRetrieves the stored item with cache behavior determined by the cache strategy.
The retrieval operation behavior varies based on the configured
CacheStrategy:Cache Strategy Behavior
NO_CACHE
Always fetches fresh data directly from the underlying storage. No cache is consulted.
let user = try await storage.get() // Always fetches from storageCACHE
Checks the cache first. If the cache contains the item, it’s returned immediately without accessing storage. If the cache is empty, the item is fetched from storage but not cached (cache is only populated on save).
let user = try await storage.get() // First checks cache, falls back to storage if cache is emptyCACHE_ON_FAILURE
Attempts to fetch from storage first. On success, the retrieved item is cached for future fallback scenarios. On failure, falls back to the cached item if available. This provides resilience against intermittent storage failures.
let user = try await storage.get() // On success: Returns from storage and caches for future failures // On failure: Returns cached item if available, otherwise throws errorNull Values
Returns
nilif no item is stored and no cached item exists. This is not considered an error condition.Thread Safety
This method is thread-safe and can be called concurrently from multiple tasks. The underlying cache operations are protected by Swift actors.
Error Handling
- For
NO_CACHEandCACHE: Errors from storage are propagated to the caller. - For
CACHE_ON_FAILURE: Errors from storage trigger cache fallback. If cache is also empty, the storage error is propagated.
Example
// With CACHE_ON_FAILURE strategy let storage = StorageDelegate( delegate: NetworkKeychain(account: "token"), cacheStrategy: .CACHE_ON_FAILURE ) // First successful get caches the result let token1 = try await storage.get() // Fetches and caches // Later, if storage fails, cache provides fallback let token2 = try await storage.get() // Returns cached tokenThrows
Any error thrown by the underlying storage, unless using
CACHE_ON_FAILUREstrategy with a valid cache.See also
Declaration
Swift
public func get() async throws -> T?Return Value
The stored item if it exists,
nilif no item is stored or cached. - For
-
delete()AsynchronousDeletes the stored item from both storage and cache.
This operation removes the item from the underlying storage and clears any cached copy, regardless of the cache strategy being used.
Behavior
- Deletes the item from the underlying storage
- Clears the cache if any caching strategy is enabled
Both operations are performed even if one fails, ensuring consistent state.
Cache Strategy Impact
All cache strategies behave identically for delete operations:
- NO_CACHE: Deletes from storage only (no cache to clear)
- CACHE: Deletes from storage and clears cache
- CACHE_ON_FAILURE: Deletes from storage and clears cache
Thread Safety
This method is thread-safe and can be called concurrently from multiple tasks. The underlying cache operations are protected by Swift actors.
Example
try await storage.save(item: user) let retrieved = try await storage.get() // Returns user (possibly cached) try await storage.delete() let afterDelete = try await storage.get() // Returns nilThrows
Any error thrown by the underlying storage implementation during deletion.
Note
After deletion, subsequent
get()calls will returnniluntil a new item is saved.See also
Declaration
Swift
public func delete() async throws
View on GitHub