-
Notifications
You must be signed in to change notification settings - Fork 49
CacheStorage (2/3) #569
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: maxg/cache_1_equivalency
Are you sure you want to change the base?
CacheStorage (2/3) #569
Changes from 2 commits
84fe9bb
6ad49cb
fa89474
dc4f9de
f615bb3
50fee4e
62c9d44
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| import Foundation | ||
|
|
||
| /// Types conforming to this protocol can be used as keys in `CacheStorage`. | ||
| /// | ||
| /// Using a type as the key allows us to strongly type each value, with the | ||
| /// key's `CacheKey.Value` associated value. | ||
| /// | ||
| /// ## Example | ||
| /// | ||
| /// Usually a key is implemented with an uninhabited type, such an empty enum. | ||
| /// | ||
| /// enum WidgetCountsKey: CacheKey { | ||
| /// static let emptyValue: [WidgetID: Int] = [:] | ||
| /// } | ||
| /// | ||
| /// You can write a small extension on `CacheStorage` to make it easier to use your key. | ||
| /// | ||
| /// extension CacheStorage { | ||
| /// var widgetCounts: [WidgetID: Int] { | ||
| /// get { self[WidgetCountsKey.self] } | ||
| /// set { self[WidgetCountsKey.self] = newValue } | ||
| /// } | ||
| /// } | ||
| public protocol CacheKey { | ||
| associatedtype Value | ||
| static var emptyValue: Self.Value { get } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,89 @@ | ||
| import Foundation | ||
| #if canImport(UIKit) | ||
| import UIKit | ||
| #endif | ||
|
|
||
| /// Environment-associated storage used to cache types used across layout passes (eg, size calculations). | ||
| /// The storage itself is type-agnostic, requiring only that its keys and values conform to the `CacheKey` protocol | ||
| /// Caches are responsible for managing their own lifetimes and eviction strategies. | ||
| @_spi(CacheStorage) public final class CacheStorage: Sendable, CustomDebugStringConvertible { | ||
maxg-square marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| // Optional name to distinguish between instances for debugging purposes. | ||
| public var name: String? = nil | ||
| fileprivate var storage: [ObjectIdentifier: Any] = [:] | ||
|
|
||
| init() { | ||
| #if canImport(UIKit) | ||
| NotificationCenter.default.addObserver( | ||
| forName: UIApplication.didReceiveMemoryWarningNotification, | ||
| object: nil, | ||
| queue: .main | ||
| ) { [weak self] _ in | ||
| self?.storage.removeAll() | ||
| } | ||
| #endif | ||
| } | ||
|
|
||
| public subscript<KeyType>(key: KeyType.Type) -> KeyType.Value where KeyType: CacheKey { | ||
| get { | ||
| storage[ObjectIdentifier(key), default: KeyType.emptyValue] as! KeyType.Value | ||
| } | ||
| set { | ||
| storage[ObjectIdentifier(key)] = newValue | ||
| } | ||
| } | ||
|
|
||
| public var debugDescription: String { | ||
| let debugName = if let name { | ||
| "CacheStorage (\(name))" | ||
| } else { | ||
| "CacheStorage" | ||
| } | ||
| return "\(debugName): \(storage.count) entries" | ||
| } | ||
|
|
||
| } | ||
|
|
||
| extension Environment { | ||
|
|
||
| struct CacheStorageEnvironmentKey: InternalEnvironmentKey { | ||
| static var defaultValue = CacheStorage() | ||
|
||
| } | ||
|
|
||
|
|
||
| @_spi(CacheStorage) public var cacheStorage: CacheStorage { | ||
| get { self[CacheStorageEnvironmentKey.self] } | ||
| set { self[CacheStorageEnvironmentKey.self] = newValue } | ||
| } | ||
|
|
||
| } | ||
|
|
||
| /// A UUID that changes based on value changes of the containing type. | ||
| /// Two fingerprinted objects may be quickly compared for equality by comparing their fingerprints. | ||
| /// This is roughly analagous to a hash, although with inverted properties: Two objects with the same fingerprint can be trivially considered equal, but two otherwise equal objects may have different fingerprint. | ||
| /// - Note: This type is deliberately NOT equatable – this is to prevent accidental inclusion of it when its containing type is equatable. | ||
| struct ComparableFingerprint: ContextuallyEquivalent, CustomStringConvertible { | ||
|
|
||
| typealias Value = UUID | ||
|
|
||
| var value: Value | ||
|
|
||
| init() { | ||
| value = Value() | ||
| } | ||
|
|
||
| mutating func modified() { | ||
| value = Value() | ||
| } | ||
|
|
||
| /// - Note: This is a duplicate message but: this type is deliberately NOT equatable – this is to prevent accidental inclusion of it when its containing type is equatable. Use this instead. | ||
| func isEquivalent(to other: ComparableFingerprint?, in context: EquivalencyContext) -> Bool { | ||
| value == other?.value | ||
| } | ||
|
|
||
| var description: String { | ||
| value.uuidString | ||
| } | ||
|
|
||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,198 @@ | ||
| import Foundation | ||
|
|
||
| /// Validating cache is a cache which, if it has a value for a key, runs a closure to verify that the cache value is still relevant and not state. | ||
| /// This is useful for cases when you might otherwise wish to store the validation data as a key, but it does not conform to Hashable, or its hashability properties do not neccessarily affect the validity of the cached data. | ||
|
||
| @_spi(CacheStorage) public struct ValidatingCache<Key, Value, ValidationData>: Sendable where Key: Hashable { | ||
|
|
||
| private var storage: [Key: ValueStorage] = [:] | ||
|
|
||
| private struct ValueStorage { | ||
| let value: Value | ||
| let validationData: ValidationData | ||
| } | ||
|
|
||
| public init() {} | ||
|
|
||
| /// Retrieves the value for a given key, without evaluating any validation conditions. | ||
| public subscript(uncheckedKey key: Key) -> Value? { | ||
| storage[key]?.value | ||
| } | ||
|
|
||
| /// Retrieves or creates a value based on a key and validation function. | ||
| /// - Parameters: | ||
| /// - key: The key to look up. | ||
| /// - validate: A function that evaluates whether or not a given result is still valid. | ||
| /// - create: Creates a fresh cache entry no valid cached data is available, and stores it. | ||
| /// - Returns: Either a cached or newly created value. | ||
| public mutating func retrieveOrCreate( | ||
| key: Key, | ||
| validate: (ValidationData) -> Bool, | ||
| create: () -> (Value, ValidationData) | ||
| ) -> Value { | ||
| if let valueStorage = storage[key] { | ||
| Logger.logValidatingCacheKeyHit(key: key) | ||
| let validationToken = Logger.logValidatingCacheValidationStart(key: key) | ||
| if validate(valueStorage.validationData) { | ||
| Logger.logValidatingCacheHitAndValidationSuccess(key: key) | ||
| Logger.logValidatingCacheValidationEnd(validationToken, key: key) | ||
| return valueStorage.value | ||
| #if DEBUG | ||
| // FIXME: WAY TO MAKE SURE THIS DOESN'T SHIP ON. | ||
| // Enable this to always evaluate the create block to assert that the caching is producing the expected value. | ||
| // if let stored = valueStorage.value as? (any Equatable) { | ||
| // let fresh = create().0 as! Equatable | ||
| // assert(stored.isEqual(fresh)) | ||
| // } | ||
| // return valueStorage.value | ||
| #endif | ||
| } else { | ||
| Logger.logValidatingCacheHitAndValidationFailure(key: key) | ||
| Logger.logValidatingCacheValidationEnd(validationToken, key: key) | ||
| } | ||
| } else { | ||
| Logger.logValidatingCacheKeyMiss(key: key) | ||
| } | ||
| let createToken = Logger.logValidatingCacheFreshValueCreationStart(key: key) | ||
| let (fresh, validationData) = create() | ||
| Logger.logValidatingCacheFreshValueCreationEnd(createToken, key: key) | ||
| storage[key] = ValueStorage(value: fresh, validationData: validationData) | ||
| return fresh | ||
| } | ||
|
|
||
| public mutating func removeValue(forKey key: Key) -> Value? { | ||
| storage.removeValue(forKey: key)?.value | ||
| } | ||
|
|
||
| } | ||
|
|
||
| /// A convenience wrapper around ValidatingCache which ensures that only values which were cached in equivalent environments are returned. | ||
| @_spi(CacheStorage) public struct EnvironmentValidatingCache<Key, Value>: Sendable where Key: Hashable { | ||
|
|
||
| private var backing = ValidatingCache<Key, Value, EnvironmentSnapshot>() | ||
|
|
||
| public init() {} | ||
|
|
||
| /// Retrieves or creates a value based on a key and environment validation. | ||
| /// - Parameters: | ||
| /// - key: The key to look up. | ||
| /// - environment: The current environment. A frozen version of this environment will be preserved along with freshly cached values for comparison. | ||
| /// - context: The equivalency context in which the environment should be evaluated. | ||
| /// - create: Creates a fresh cache entry no valid cached data is available, and stores it. | ||
| /// - Returns: Either a cached or newly created value. | ||
| mutating func retrieveOrCreate( | ||
| key: Key, | ||
| environment: Environment, | ||
| context: EquivalencyContext, | ||
| create: (Environment) -> Value | ||
| ) -> Value { | ||
| backing.retrieveOrCreate(key: key) { | ||
| environment.isEquivalent(to: $0, in: context) | ||
| } create: { | ||
| environment.snapshottingAccess { environment in | ||
| create(environment) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| } | ||
|
|
||
| /// A convenience wrapper around ValidatingCache which ensures that only values which were cached in equivalent environments are returned, and allows for additional data to be stored to be validated. | ||
| @_spi(CacheStorage) public struct EnvironmentAndValueValidatingCache<Key, Value, AdditionalValidationData>: Sendable where Key: Hashable { | ||
|
|
||
| private var backing = ValidatingCache<Key, Value, (EnvironmentSnapshot, AdditionalValidationData)>() | ||
|
|
||
| public init() {} | ||
|
|
||
| /// Retrieves or creates a value based on a key and a validation function, alongside environment validation. | ||
| /// - Parameters: | ||
| /// - key: The key to look up. | ||
| /// - environment: The current environment. A frozen version of this environment will be preserved along with freshly cached values for comparison. | ||
| /// - context: The equivalency context in which the environment should be evaluated. | ||
| /// - validate: A function that evaluates whether or not a given result is still valid. | ||
| /// - create: Creates a fresh cache entry no valid cached data is available, and stores it. | ||
| /// - Returns: Either a cached or newly created value. | ||
| /// - Note: Generally, prefer the `validationValue` versions of this method if the validation value conforms to ContextuallyEquivalent or Equatable. | ||
| mutating func retrieveOrCreate( | ||
| key: Key, | ||
| environment: Environment, | ||
| context: EquivalencyContext, | ||
| validate: (AdditionalValidationData) -> Bool, | ||
| create: (Environment) -> (Value, AdditionalValidationData) | ||
| ) -> Value { | ||
| backing.retrieveOrCreate(key: key) { | ||
| environment.isEquivalent(to: $0.0, in: context) && validate($0.1) | ||
| } create: { | ||
| let ((value, additional), snapshot) = environment.snapshottingAccess { environment in | ||
| create(environment) | ||
| } | ||
| return (value, (snapshot, additional)) | ||
| } | ||
| } | ||
|
|
||
| } | ||
|
|
||
|
|
||
| @_spi(CacheStorage) extension EnvironmentAndValueValidatingCache where AdditionalValidationData: ContextuallyEquivalent { | ||
|
|
||
| /// Retrieves or creates a value based on a key and a validation value, alongside environment validation. | ||
| /// - Parameters: | ||
| /// - key: The key to look up. | ||
| /// - environment: The current environment. A frozen version of this environment will be preserved along with freshly cached values for comparison. | ||
| /// - context: The equivalency context in which the environment and validation values should be evaluated. | ||
| /// - validationValue: A value that will be compared using contextual equivalence that evaluates whether or not a given result is still valid. | ||
| /// - create: Creates a fresh cache entry no valid cached data is available, and stores it. | ||
| /// - Returns: Either a cached or newly created value. | ||
| public mutating func retrieveOrCreate( | ||
| key: Key, | ||
| environment: Environment, | ||
| validationValue: AdditionalValidationData, | ||
| context: EquivalencyContext, | ||
| create: (Environment) -> (Value) | ||
| ) -> Value { | ||
| retrieveOrCreate(key: key, environment: environment, context: context) { | ||
| $0.isEquivalent(to: validationValue, in: context) | ||
| } create: { | ||
| (create($0), validationValue) | ||
| } | ||
|
|
||
| } | ||
|
|
||
| } | ||
|
|
||
| @_spi(CacheStorage) extension EnvironmentAndValueValidatingCache where AdditionalValidationData: Equatable { | ||
|
|
||
| /// Retrieves or creates a value based on a key and a validation value, alongside environment validation. | ||
| /// - Parameters: | ||
| /// - key: The key to look up. | ||
| /// - environment: The current environment. A frozen version of this environment will be preserved along with freshly cached values for comparison. | ||
| /// - context: The equivalency context in which the environment should be evaluated. | ||
| /// - validationValue: A value that will be compared using strict equality that evaluates whether or not a given result is still valid. | ||
| /// - create: Creates a fresh cache entry no valid cached data is available, and stores it. | ||
| /// - Returns: Either a cached or newly created value. | ||
| @_disfavoredOverload public mutating func retrieveOrCreate( | ||
| key: Key, | ||
| environment: Environment, | ||
| validationValue: AdditionalValidationData, | ||
| context: EquivalencyContext, | ||
| create: (Environment) -> (Value) | ||
| ) -> Value { | ||
| retrieveOrCreate(key: key, environment: environment, context: context) { | ||
| $0 == validationValue | ||
| } create: { | ||
| (create($0), validationValue) | ||
| } | ||
| } | ||
|
|
||
| } | ||
|
|
||
|
|
||
| extension Equatable { | ||
|
|
||
| fileprivate func isEqual(_ other: any Equatable) -> Bool { | ||
| guard let other = other as? Self else { | ||
| return false | ||
| } | ||
| return self == other | ||
| } | ||
|
|
||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.