Skip to content

Commit

Permalink
feat: Differentiate OF non-OF in metadata
Browse files Browse the repository at this point in the history
  • Loading branch information
fabriziodemaria committed Jun 7, 2024
1 parent d295ffa commit fa3bb5d
Show file tree
Hide file tree
Showing 9 changed files with 100 additions and 56 deletions.
88 changes: 62 additions & 26 deletions Sources/Confidence/Confidence.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ public class Confidence: ConfidenceEventSender {
public var region: ConfidenceRegion
private let parent: ConfidenceContextProvider?
private let eventSenderEngine: EventSenderEngine
private let contextSubject = CurrentValueSubject<ConfidenceStruct, Never>([:])
private let contextSubject = CurrentValueSubject<Confidence.ContextUpdateSignal, Never>(
ContextUpdateSignal.init(context: [:], isProvider: false))
private var removedContextKeys: Set<String> = Set()
private let confidenceQueue = DispatchQueue(label: "com.confidence.queue")
private let remoteFlagResolver: ConfidenceResolveClient
Expand All @@ -33,7 +34,7 @@ public class Confidence: ConfidenceEventSender {
self.clientSecret = clientSecret
self.region = region
self.storage = storage
self.contextSubject.value = context
self.contextSubject.value = ContextUpdateSignal.init(context: context, isProvider: false)
self.parent = parent
self.storage = storage
self.flagApplier = flagApplier
Expand All @@ -42,15 +43,15 @@ public class Confidence: ConfidenceEventSender {
putContext(context: ["visitor_id": ConfidenceValue.init(string: visitorId)])
}

contextChanges().sink { [weak self] context in
contextChanges().sink { [weak self] signal in
guard let self = self else {
return
}
self.currentFetchTask?.cancel()
self.currentFetchTask = Task {
do {
let context = self.getContext()
try await self.fetchAndActivate()
try await self.fetchAndActivate(isProvider: signal.isProvider)
self.contextReconciliatedChanges.send(context.hash())
} catch {
}
Expand All @@ -64,14 +65,20 @@ public class Confidence: ConfidenceEventSender {
self.cache = savedFlags
}

public func fetchAndActivate() async throws {
try await internalFetch()
/**
Fetches flag evaluations from backend and returns once the fetch is
complete (regardless of success)
Args:
- isProvider: used in case of OpenFeature integration, do not override manually
*/
public func fetchAndActivate(isProvider: Bool = false) async throws {
try await internalFetch(isProvider: isProvider)
try activate()
}

func internalFetch() async throws {
func internalFetch(isProvider: Bool) async throws {
let context = getContext()
let resolvedFlags = try await remoteFlagResolver.resolve(ctx: context)
let resolvedFlags = try await remoteFlagResolver.resolve(ctx: context, isProvider: isProvider)
let resolution = FlagResolution(
context: context,
flags: resolvedFlags.resolvedValues,
Expand All @@ -80,24 +87,39 @@ public class Confidence: ConfidenceEventSender {
try storage.save(data: resolution)
}

public func asyncFetch() {
/**
Start a network request to fetch flag evaluations from backend, returns immediately.
In case of success storage cache will be updated for future sessions, but in-session cache remains unchanged.
Args:
- isProvider: used in case of OpenFeature integration, do not override manually
*/
public func asyncFetch(isProvider: Bool = false) {
Task {
try await internalFetch()
try await internalFetch(isProvider: isProvider)
}
}

public func getEvaluation<T>(key: String, defaultValue: T) throws -> Evaluation<T> {
/**
Read flag evaluation from local cache. Does not trigger a backend request.
Args:
- key: flag name followed by the path of the property you want to access (dot notation expected).
e.g. "my-flag.my-property"
- defaultValue: value return in case of errors, for example if the flag/property is not found
- isProvider: used in case of OpenFeature integration, do not override manually
*/
public func getEvaluation<T>(key: String, defaultValue: T, isProvider: Bool = false) throws -> Evaluation<T> {
try self.cache.evaluate(
flagName: key,
defaultValue: defaultValue,
context: getContext(),
flagApplier: flagApplier
flagApplier: flagApplier,
isProvider: isProvider
)
}

public func getValue<T>(key: String, defaultValue: T) -> T {
do {
return try getEvaluation(key: key, defaultValue: defaultValue).value
return try getEvaluation(key: key, defaultValue: defaultValue, isProvider: false).value
} catch {
return defaultValue
}
Expand All @@ -107,7 +129,7 @@ public class Confidence: ConfidenceEventSender {
return storage.isEmpty()
}

public func contextChanges() -> AnyPublisher<ConfidenceStruct, Never> {
private func contextChanges() -> AnyPublisher<Confidence.ContextUpdateSignal, Never> {
return contextSubject
.dropFirst()
.removeDuplicates()
Expand Down Expand Up @@ -172,48 +194,53 @@ public class Confidence: ConfidenceEventSender {
var reconciledCtx = parentContext.filter {
!removedContextKeys.contains($0.key)
}
self.contextSubject.value.forEach { entry in
self.contextSubject.value.context.forEach { entry in
reconciledCtx.updateValue(entry.value, forKey: entry.key)
}
return reconciledCtx
}

public func putContext(key: String, value: ConfidenceValue) {
withLock { confidence in
var map = confidence.contextSubject.value
var map = confidence.contextSubject.value.context
map[key] = value
confidence.contextSubject.value = map
confidence.contextSubject.value = ContextUpdateSignal(context: map, isProvider: false)
}
}

public func putContext(context: ConfidenceStruct) {
withLock { confidence in
var map = confidence.contextSubject.value
var map = confidence.contextSubject.value.context
for entry in context {
map.updateValue(entry.value, forKey: entry.key)
}
confidence.contextSubject.value = map
confidence.contextSubject.value = ContextUpdateSignal(context: map, isProvider: false)
}
}

public func putContext(context: ConfidenceStruct, removeKeys removedKeys: [String] = []) {
/**
Updates the context. This will trigger a new fetchAndActivate thus updating the cache mid-session.
Args:
- isProvider: used in case of OpenFeature integration, do not override manually
*/
public func putContext(context: ConfidenceStruct, removeKeys removedKeys: [String] = [], isProvider: Bool = false) {
withLock { confidence in
var map = confidence.contextSubject.value
var map = confidence.contextSubject.value.context
for removedKey in removedKeys {
map.removeValue(forKey: removedKey)
}
for entry in context {
map.updateValue(entry.value, forKey: entry.key)
}
confidence.contextSubject.value = map
confidence.contextSubject.value = ContextUpdateSignal.init(context: map, isProvider: isProvider)
}
}

public func removeKey(key: String) {
withLock { confidence in
var map = confidence.contextSubject.value
var map = confidence.contextSubject.value.context
map.removeValue(forKey: key)
confidence.contextSubject.value = map
confidence.contextSubject.value = ContextUpdateSignal(context: map, isProvider: false)
confidence.removedContextKeys.insert(key)
}
}
Expand Down Expand Up @@ -243,7 +270,7 @@ extension Confidence {
var visitorId = VisitorUtil().getId()
var initialContext: ConfidenceStruct = [:]

/**
/***
Initializes the builder with the given credentails.
*/
public init(clientSecret: String) {
Expand Down Expand Up @@ -276,7 +303,7 @@ extension Confidence {
return self
}

/**
/***
Sets the region for the network request to the Confidence backend.
The default is `global` and the requests are automatically routed to the closest server.
*/
Expand Down Expand Up @@ -325,4 +352,13 @@ extension Confidence {
)
}
}

private struct ContextUpdateSignal: Equatable, Hashable {
let context: ConfidenceStruct
let isProvider: Bool

func hash(into hasher: inout Hasher) {
hasher.combine(context.hash())
}
}
}
2 changes: 1 addition & 1 deletion Sources/Confidence/ConfidenceClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ protocol ConfidenceClient {

protocol ConfidenceResolveClient {
// Async
func resolve(ctx: ConfidenceStruct) async throws -> ResolvesResult
func resolve(ctx: ConfidenceStruct, isProvider: Bool) async throws -> ResolvesResult
}

struct ResolvedValue: Codable, Equatable {
Expand Down
3 changes: 2 additions & 1 deletion Sources/Confidence/FlagEvaluation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ extension FlagResolution {
flagName: String,
defaultValue: T,
context: ConfidenceStruct,
flagApplier: FlagApplier? = nil
flagApplier: FlagApplier? = nil,
isProvider: Bool
) throws -> Evaluation<T> {
let parsedKey = try FlagPath.getPath(for: flagName)
if self == FlagResolution.EMPTY {
Expand Down
8 changes: 4 additions & 4 deletions Sources/Confidence/RemoteResolveConfidenceClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,13 @@ public class RemoteConfidenceResolveClient: ConfidenceResolveClient {

// MARK: Resolver

public func resolve(flags: [String], ctx: ConfidenceStruct) async throws -> ResolvesResult {
public func resolve(flags: [String], ctx: ConfidenceStruct, isProvider: Bool) async throws -> ResolvesResult {
let request = ResolveFlagsRequest(
flags: flags.map { "flags/\($0)" },
evaluationContext: TypeMapper.convert(structure: ctx),
clientSecret: options.credentials.getSecret(),
apply: applyOnResolve,
sdk: Sdk(id: metadata.name, version: metadata.version)
sdk: Sdk(id: isProvider ? "SDK_ID_SWIFT_PROVIDER" : metadata.name, version: metadata.version)
)

do {
Expand All @@ -54,8 +54,8 @@ public class RemoteConfidenceResolveClient: ConfidenceResolveClient {
}
}

public func resolve(ctx: ConfidenceStruct) async throws -> ResolvesResult {
return try await resolve(flags: [], ctx: ctx)
public func resolve(ctx: ConfidenceStruct, isProvider: Bool) async throws -> ResolvesResult {
return try await resolve(flags: [], ctx: ctx, isProvider: isProvider)
}

// MARK: Private
Expand Down
19 changes: 11 additions & 8 deletions Sources/ConfidenceProvider/ConfidenceFeatureProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,10 @@ public class ConfidenceFeatureProvider: FeatureProvider {
if initializationStrategy == .activateAndFetchAsync {
try confidence.activate()
eventHandler.send(.ready)
confidence.asyncFetch()
confidence.asyncFetch(isProvider: true)
} else {
Task {
try await confidence.fetchAndActivate()
try await confidence.fetchAndActivate(isProvider: true)
eventHandler.send(.ready)
}
}
Expand Down Expand Up @@ -85,37 +85,40 @@ public class ConfidenceFeatureProvider: FeatureProvider {
}

private func updateConfidenceContext(context: EvaluationContext, removedKeys: [String] = []) {
confidence.putContext(context: ConfidenceTypeMapper.from(ctx: context), removeKeys: removedKeys)
confidence.putContext(
context: ConfidenceTypeMapper.from(ctx: context),
removeKeys: removedKeys,
isProvider: true)
}

public func getBooleanEvaluation(key: String, defaultValue: Bool, context: EvaluationContext?) throws
-> OpenFeature.ProviderEvaluation<Bool>
{
try confidence.getEvaluation(key: key, defaultValue: defaultValue).toProviderEvaluation()
try confidence.getEvaluation(key: key, defaultValue: defaultValue, isProvider: true).toProviderEvaluation()
}

public func getStringEvaluation(key: String, defaultValue: String, context: EvaluationContext?) throws
-> OpenFeature.ProviderEvaluation<String>
{
try confidence.getEvaluation(key: key, defaultValue: defaultValue).toProviderEvaluation()
try confidence.getEvaluation(key: key, defaultValue: defaultValue, isProvider: true).toProviderEvaluation()
}

public func getIntegerEvaluation(key: String, defaultValue: Int64, context: EvaluationContext?) throws
-> OpenFeature.ProviderEvaluation<Int64>
{
try confidence.getEvaluation(key: key, defaultValue: defaultValue).toProviderEvaluation()
try confidence.getEvaluation(key: key, defaultValue: defaultValue, isProvider: true).toProviderEvaluation()
}

public func getDoubleEvaluation(key: String, defaultValue: Double, context: EvaluationContext?) throws
-> OpenFeature.ProviderEvaluation<Double>
{
try confidence.getEvaluation(key: key, defaultValue: defaultValue).toProviderEvaluation()
try confidence.getEvaluation(key: key, defaultValue: defaultValue, isProvider: true).toProviderEvaluation()
}

public func getObjectEvaluation(key: String, defaultValue: OpenFeature.Value, context: EvaluationContext?)
throws -> OpenFeature.ProviderEvaluation<OpenFeature.Value>
{
try confidence.getEvaluation(key: key, defaultValue: defaultValue).toProviderEvaluation()
try confidence.getEvaluation(key: key, defaultValue: defaultValue, isProvider: true).toProviderEvaluation()
}

public func observe() -> AnyPublisher<OpenFeature.ProviderEvent, Never> {
Expand Down
Loading

0 comments on commit fa3bb5d

Please sign in to comment.