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

feat: Differentiate OF non-OF in metadata #138

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
90 changes: 64 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,55 @@ 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:
- context: entries to be appended to the context
- removeKeys: keys of the entries that are removed from the context (including parent entries)
- 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 +272,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 +305,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 +354,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
Loading