Skip to content
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

[#96] Asynchronous Feature-Flag Stores #129

Merged
merged 22 commits into from
May 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading
Loading