Skip to content

Commit

Permalink
[#96] Asynchronous Feature-Flag Stores (#129)
Browse files Browse the repository at this point in the history
* Extended the methods of `FeatureFlagStoreProtocol`, `MutableFeatureFlagStoreProtocol`, and `FeatureFlagResolverProtocol` with `async`
* Introduced these components’ synchronous counterparts which inherit from the asynchronous variants
* Switched the built-in feature-flag stores to the synchronous protocols
* Switched `FeatureFlag` to `SynchronousFeatureFlagResolverProtocol`
* Refactored and extended `FeatureFlagResolver`
* Extended `FeatureFlagResolverError`
* Refactored and extended tests
  • Loading branch information
yakovmanshin committed May 1, 2024
1 parent 31d6290 commit 783578b
Show file tree
Hide file tree
Showing 29 changed files with 1,868 additions and 725 deletions.
12 changes: 6 additions & 6 deletions Sources/YMFF/FeatureFlag/FeatureFlag.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ final public class FeatureFlag<RawValue, Value> {
/// The fallback value returned when no store is able to provide the real one.
public let defaultValue: Value

private let resolver: FeatureFlagResolverProtocol
private let resolver: any SynchronousFeatureFlagResolverProtocol

// MARK: Initializers

Expand All @@ -39,7 +39,7 @@ final public class FeatureFlag<RawValue, Value> {
_ key: FeatureFlagKey,
transformer: FeatureFlagValueTransformer<RawValue, Value>,
default defaultValue: Value,
resolver: FeatureFlagResolverProtocol
resolver: any SynchronousFeatureFlagResolverProtocol
) {
self.key = key
self.transformer = transformer
Expand All @@ -56,7 +56,7 @@ final public class FeatureFlag<RawValue, Value> {
public convenience init(
_ key: FeatureFlagKey,
default defaultValue: Value,
resolver: FeatureFlagResolverProtocol
resolver: any SynchronousFeatureFlagResolverProtocol
) where RawValue == Value {
self.init(key, transformer: .identity, default: defaultValue, resolver: resolver)
}
Expand All @@ -67,13 +67,13 @@ final public class FeatureFlag<RawValue, Value> {
public var wrappedValue: Value {
get {
guard
let rawValue = try? (resolver.value(for: key) as RawValue),
let rawValue = try? (resolver.valueSync(for: key) as RawValue),
let value = transformer.valueFromRawValue(rawValue)
else { return defaultValue }

return value
} set {
try? resolver.setValue(transformer.rawValueFromValue(newValue), toMutableStoreUsing: key)
try? resolver.setValueSync(transformer.rawValueFromValue(newValue), toMutableStoreUsing: key)
}
}

Expand All @@ -88,7 +88,7 @@ final public class FeatureFlag<RawValue, Value> {
///
/// + Errors thrown by `resolver` are ignored.
public func removeValueFromMutableStore() {
try? resolver.removeValueFromMutableStore(using: key)
try? resolver.removeValueFromMutableStoreSync(using: key)
}

}
174 changes: 141 additions & 33 deletions Sources/YMFF/FeatureFlagResolver/FeatureFlagResolver.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,12 @@ final public class FeatureFlagResolver {
}

deinit {
configuration.stores
.compactMap({ $0.asMutable })
.forEach({ $0.saveChanges() })
let mutableStores = getMutableStores()
Task { [mutableStores] in
for store in mutableStores {
await store.saveChanges()
}
}
}

}
Expand All @@ -50,23 +53,110 @@ final public class FeatureFlagResolver {

extension FeatureFlagResolver: FeatureFlagResolverProtocol {

public func value<Value>(for key: FeatureFlagKey) throws -> Value {
let retrievedValue: Value = try retrieveFirstValueFoundInStores(byKey: key)
public func value<Value>(for key: FeatureFlagKey) async throws -> Value {
let retrievedValue: Value = try await retrieveFirstValue(forKey: key)
try validateValue(retrievedValue)

return retrievedValue
}

public func setValue<Value>(_ newValue: Value, toMutableStoreUsing key: FeatureFlagKey) async throws {
guard !configuration.stores.isEmpty else {
throw FeatureFlagResolverError.noStoreAvailable
}

let mutableStores = getMutableStores()
guard !mutableStores.isEmpty else {
throw FeatureFlagResolverError.noMutableStoreAvailable
}

try await validateOverrideValue(newValue, forKey: key)

await mutableStores[0].setValue(newValue, forKey: key)
}

public func removeValueFromMutableStore(using key: FeatureFlagKey) async throws {
guard !configuration.stores.isEmpty else {
throw FeatureFlagResolverError.noStoreAvailable
}

let mutableStores = getMutableStores()
guard !mutableStores.isEmpty else {
throw FeatureFlagResolverError.noMutableStoreAvailable
}

await mutableStores[0].removeValue(forKey: key)
}

}

// MARK: - SynchronousFeatureFlagResolverProtocol

extension FeatureFlagResolver: SynchronousFeatureFlagResolverProtocol {

public func valueSync<Value>(for key: FeatureFlagKey) throws -> Value {
let retrievedValue: Value = try retrieveFirstValueSync(forKey: key)
try validateValue(retrievedValue)

return retrievedValue
}

public func setValue<Value>(_ newValue: Value, toMutableStoreUsing key: FeatureFlagKey) throws {
try validateOverrideValue(newValue, forKey: key)
public func setValueSync<Value>(_ newValue: Value, toMutableStoreUsing key: FeatureFlagKey) throws {
guard !configuration.stores.isEmpty else {
throw FeatureFlagResolverError.noStoreAvailable
}

guard !getSyncStores().isEmpty else {
throw FeatureFlagResolverError.noSyncStoreAvailable
}

let mutableStores = getSyncMutableStores()
guard !mutableStores.isEmpty else {
throw FeatureFlagResolverError.noSyncMutableStoreAvailable
}

try validateOverrideValueSync(newValue, forKey: key)

mutableStores[0].setValueSync(newValue, forKey: key)
}

public func removeValueFromMutableStoreSync(using key: FeatureFlagKey) throws {
guard !configuration.stores.isEmpty else {
throw FeatureFlagResolverError.noStoreAvailable
}

let mutableStore = try findMutableStores()[0]
mutableStore.setValue(newValue, forKey: key)
guard !getSyncStores().isEmpty else {
throw FeatureFlagResolverError.noSyncStoreAvailable
}

let mutableStores = getSyncMutableStores()
guard !mutableStores.isEmpty else {
throw FeatureFlagResolverError.noSyncMutableStoreAvailable
}

mutableStores[0].removeValueSync(forKey: key)
}

}

// MARK: - Stores

extension FeatureFlagResolver {

private func getStores() -> [any FeatureFlagStoreProtocol] {
configuration.stores.map { $0.asImmutable }
}

public func removeValueFromMutableStore(using key: FeatureFlagKey) throws {
let mutableStore = try firstMutableStore(withValueForKey: key)
mutableStore.removeValue(forKey: key)
private func getSyncStores() -> [any SynchronousFeatureFlagStoreProtocol] {
getStores().compactMap { $0 as? SynchronousFeatureFlagStoreProtocol }
}

private func getMutableStores() -> [any MutableFeatureFlagStoreProtocol] {
getStores().compactMap { $0 as? MutableFeatureFlagStoreProtocol }
}

private func getSyncMutableStores() -> [any SynchronousMutableFeatureFlagStoreProtocol] {
getStores().compactMap { $0 as? SynchronousMutableFeatureFlagStoreProtocol }
}

}
Expand All @@ -75,14 +165,38 @@ extension FeatureFlagResolver: FeatureFlagResolverProtocol {

extension FeatureFlagResolver {

func retrieveFirstValueFoundInStores<Value>(byKey key: String) throws -> Value {
private func retrieveFirstValue<Value>(forKey key: String) async throws -> Value {
guard !configuration.stores.isEmpty else {
throw FeatureFlagResolverError.noStoreAvailable
}

let matchingStores = getStores()

for store in matchingStores {
if await store.containsValue(forKey: key) {
guard let value: Value = await store.value(forKey: key)
else { throw FeatureFlagResolverError.typeMismatch }

return value
}
}

throw FeatureFlagResolverError.valueNotFoundInPersistentStores(key: key)
}

private func retrieveFirstValueSync<Value>(forKey key: String) throws -> Value {
guard !configuration.stores.isEmpty else {
throw FeatureFlagResolverError.noStoreAvailable
}

for store in configuration.stores {
if store.asImmutable.containsValue(forKey: key) {
guard let value: Value = store.asImmutable.value(forKey: key)
let matchingStores = getSyncStores()
guard !matchingStores.isEmpty else {
throw FeatureFlagResolverError.noSyncStoreAvailable
}

for store in matchingStores {
if store.containsValueSync(forKey: key) {
guard let value: Value = store.valueSync(forKey: key)
else { throw FeatureFlagResolverError.typeMismatch }

return value
Expand All @@ -108,11 +222,11 @@ extension FeatureFlagResolver {

extension FeatureFlagResolver {

func validateOverrideValue<Value>(_ value: Value, forKey key: FeatureFlagKey) throws {
private func validateOverrideValue<Value>(_ value: Value, forKey key: FeatureFlagKey) async throws {
try validateValue(value)

do {
let _: Value = try retrieveFirstValueFoundInStores(byKey: key)
let _: Value = try await retrieveFirstValue(forKey: key)
} catch FeatureFlagResolverError.valueNotFoundInPersistentStores {
// If none of the persistent stores contains a value for the key, then the client is attempting
// to set a new value (instead of overriding an existing one). That’s an acceptable use case.
Expand All @@ -121,23 +235,17 @@ extension FeatureFlagResolver {
}
}

private func firstMutableStore(withValueForKey key: String) throws -> MutableFeatureFlagStoreProtocol {
let mutableStores = try findMutableStores()

guard let firstStoreWithValueForKey = mutableStores.first(where: { $0.containsValue(forKey: key) }) else {
throw FeatureFlagResolverError.noMutableStoreContainsValueForKey(key: key)
}
return firstStoreWithValueForKey
}

private func findMutableStores() throws -> [MutableFeatureFlagStoreProtocol] {
let stores = configuration.stores.compactMap({ $0.asMutable })
func validateOverrideValueSync<Value>(_ value: Value, forKey key: FeatureFlagKey) throws {
try validateValue(value)

if stores.isEmpty {
throw FeatureFlagResolverError.noMutableStoreAvailable
do {
let _: Value = try retrieveFirstValueSync(forKey: key)
} catch FeatureFlagResolverError.valueNotFoundInPersistentStores {
// If none of the persistent stores contains a value for the key, then the client is attempting
// to set a new value (instead of overriding an existing one). That’s an acceptable use case.
} catch {
throw error
}

return stores
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,12 @@

/// Errors returned by `FeatureFlagResolver`.
public enum FeatureFlagResolverError: Error {
case noMutableStoreAvailable
case noMutableStoreContainsValueForKey(key: String)
case noStoreAvailable
case noSyncStoreAvailable
case noMutableStoreAvailable
case noSyncMutableStoreAvailable
case optionalValuesNotAllowed
case typeMismatch
case valueNotFoundInPersistentStores(key: String)
case noMutableStoreContainsValueForKey(key: String)
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,31 +15,31 @@ import YMFFProtocols
/// A YMFF-supplied implementation of the object that stores feature flag values used in runtime.
final public class RuntimeOverridesStore {

private var store: TransparentFeatureFlagStore
var store: TransparentFeatureFlagStore

public init() {
store = .init()
}

}

// MARK: - MutableFeatureFlagStoreProtocol
// MARK: - SynchronousMutableFeatureFlagStoreProtocol

extension RuntimeOverridesStore: MutableFeatureFlagStoreProtocol {
extension RuntimeOverridesStore: SynchronousMutableFeatureFlagStoreProtocol {

public func containsValue(forKey key: String) -> Bool {
public func containsValueSync(forKey key: String) -> Bool {
store[key] != nil
}

public func value<Value>(forKey key: String) -> Value? {
public func valueSync<Value>(forKey key: String) -> Value? {
store[key] as? Value
}

public func setValue<Value>(_ value: Value, forKey key: String) {
public func setValueSync<Value>(_ value: Value, forKey key: String) {
store[key] = value
}

public func removeValue(forKey key: String) {
public func removeValueSync(forKey key: String) {
store.removeValue(forKey: key)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,17 @@ import YMFFProtocols
// MARK: - TransparentFeatureFlagStore

/// A simple dictionary used to store and retrieve feature flag values.
public typealias TransparentFeatureFlagStore = [String : Any]
public typealias TransparentFeatureFlagStore = [String: Any]

// MARK: - FeatureFlagStoreProtocol
// MARK: - SynchronousFeatureFlagStoreProtocol

extension TransparentFeatureFlagStore: FeatureFlagStoreProtocol {
extension TransparentFeatureFlagStore: SynchronousFeatureFlagStoreProtocol, FeatureFlagStoreProtocol {

public func containsValue(forKey key: String) -> Bool {
public func containsValueSync(forKey key: String) -> Bool {
self[key] != nil
}

public func value<V>(forKey key: String) -> V? {
public func valueSync<V>(forKey key: String) -> V? {
self[key] as? V
}

Expand Down
14 changes: 7 additions & 7 deletions Sources/YMFF/FeatureFlagResolver/Store/UserDefaultsStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,27 +30,27 @@ final public class UserDefaultsStore {

}

// MARK: - MutableFeatureFlagStoreProtocol
// MARK: - SynchronousMutableFeatureFlagStoreProtocol

extension UserDefaultsStore: MutableFeatureFlagStoreProtocol {
extension UserDefaultsStore: SynchronousMutableFeatureFlagStoreProtocol {

public func containsValue(forKey key: String) -> Bool {
public func containsValueSync(forKey key: String) -> Bool {
userDefaults.object(forKey: key) != nil
}

public func value<Value>(forKey key: String) -> Value? {
public func valueSync<Value>(forKey key: String) -> Value? {
userDefaults.object(forKey: key) as? Value
}

public func setValue<Value>(_ value: Value, forKey key: String) {
public func setValueSync<Value>(_ value: Value, forKey key: String) {
userDefaults.set(value, forKey: key)
}

public func removeValue(forKey key: String) {
public func removeValueSync(forKey key: String) {
userDefaults.removeObject(forKey: key)
}

public func saveChanges() {
public func saveChangesSync() {
userDefaults.synchronize()
}

Expand Down

0 comments on commit 783578b

Please sign in to comment.