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.

Parameters

T

The type of the object being stored. Must conform to Codable and Sendable to ensure safe encoding/decoding and concurrent access.

  • 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 token
    

    See also

    CacheStrategy

    Declaration

    Swift

    public init(delegate: any Storage<T>, cacheStrategy: CacheStrategy = .NO_CACHE)

    Parameters

    delegate

    The underlying storage to delegate operations to. This can be any type conforming to Storage<T>, such as Memory<T>, Keychain<T>, or custom implementations.

    cacheStrategy

    The caching strategy to use. Defaults to .NO_CACHE for 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 only
    

    CACHE

    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 item
    

    CACHE_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.

    Declaration

    Swift

    public func save(item: T) async throws

    Parameters

    item

    The item to save. Must conform to Codable and Sendable.

  • get() Asynchronous

    Retrieves 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 storage
    

    CACHE

    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 empty
    

    CACHE_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 error
    

    Null Values

    Returns nil if 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_CACHE and CACHE: 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 token
    

    Throws

    Any error thrown by the underlying storage, unless using CACHE_ON_FAILURE strategy with a valid cache.

    Declaration

    Swift

    public func get() async throws -> T?

    Return Value

    The stored item if it exists, nil if no item is stored or cached.

  • delete() Asynchronous

    Deletes 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

    1. Deletes the item from the underlying storage
    2. 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 nil
    

    Throws

    Any error thrown by the underlying storage implementation during deletion.

    Note

    After deletion, subsequent get() calls will return nil until a new item is saved.

    See also

    save(item:), get()

    Declaration

    Swift

    public func delete() async throws