Skip to content
Merged
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
2 changes: 1 addition & 1 deletion .github/workflows/sonar.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ jobs:
build-and-collect-coverage:
name: Build & Collect Coverage
runs-on: macos-15-xlarge
timeout-minutes: 25
timeout-minutes: 40
steps:
- name: Checkout
uses: actions/checkout@v4
Expand Down
2 changes: 1 addition & 1 deletion Split/Api/DefaultSplitClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ extension DefaultSplitClient {
}
}

private func registerEvent<T: EventMetadata>(_ event: SplitEvent, action: @escaping (T) -> Void) {
private func registerEvent<T: EventMetadata>(_ event: SplitEvent, action: @Sendable @escaping (T) -> Void) {
guard let factory = clientManager?.splitFactory else { return }
let task = SplitEventActionTask(action: action, event: event, runInBackground: true, factory: factory, queue: nil)
eventsManager.register(event: event, task: task)
Expand Down
2 changes: 1 addition & 1 deletion Split/Api/LocalhostSplitClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ public final class LocalhostSplitClient: NSObject, SplitClient {
}
}

private func registerEvent<T: EventMetadata>(_ event: SplitEvent, action: @escaping (T) -> Void) {
private func registerEvent<T: EventMetadata>(_ event: SplitEvent, action: @Sendable @escaping (T) -> Void) {
guard let factory = clientManager?.splitFactory else { return }
let task = SplitEventActionTask(action: action, event: event, runInBackground: true, factory: factory, queue: nil)
eventsManager?.register(event: event, task: task)
Expand Down
3 changes: 1 addition & 2 deletions Split/Api/SplitClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,8 @@
import Foundation

public typealias SplitAction = () -> Void
public typealias SplitActionWithMetadata<T: EventMetadata> = (T) -> Void

@objc public protocol SplitClientEventListener: AnyObject {
@objc public protocol SplitClientEventListener: AnyObject, Sendable {
@objc(onSdkReady:)
optional func onSdkReady(_ metadata: SdkReadyMetadata)
@objc(onSdkReadyFromCache:)
Expand Down
6 changes: 3 additions & 3 deletions Split/Engine/FallbackTreatments/FallbackTreatments.swift
Original file line number Diff line number Diff line change
Expand Up @@ -70,16 +70,16 @@ import Foundation
global = sanitizedGlobal
return self
}

// MARK: By Flag
@objc(byFlag:)
public func byFlag(_ byFlagFallbacks: [String: FallbackTreatment]) -> Builder {

// Warn if you're overriding an already configured flag
for key in byFlagFallbacks.keys where byFlag.keys.contains(key) {
Logger.w("Duplicate fallback for flag '\(key)'. Overriding existing value.")
}

// Merge
var merged = byFlag
for (key, value) in byFlagFallbacks {
Expand Down
2 changes: 1 addition & 1 deletion Split/Events/EventsManagerCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ class MainSplitEventsManager: SplitEventsManagerCoordinator, @unchecked Sendable

self.triggered.insert(event.type)
self.managers.forEach { _, manager in
manager.notifyInternalEvent(event.type)
manager.notifyInternalEvent(event)
}
}
}
Expand Down
34 changes: 24 additions & 10 deletions Split/Events/SplitEventActionTask.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@

import Foundation

internal typealias SplitActionWithMetadata<T: EventMetadata> = @Sendable (T) -> ()

class SplitEventActionTask: SplitEventTask, @unchecked Sendable {

private var eventHandler: SplitAction?
private var eventHandlerWithMetadata: SplitActionWithMetadata<EventMetadata>?
private var eventHandlerWithMetadata: EventMetadataHandler?
private var queue: DispatchQueue?
var event: SplitEvent
var runInBackground: Bool = false
Expand All @@ -18,14 +20,8 @@ class SplitEventActionTask: SplitEventTask, @unchecked Sendable {
self.queue = queue
self.factory = factory

// Metadata: "swap" for concrete type and ensure type is correct for this event
self.eventHandlerWithMetadata = { metadata in
guard let typed = metadata as? T else {
Logger.e("Wrong metadata type for this event (\(event.toString())).")
return
}
action(typed)
}
// Preserve the concrete type using type erasure container
self.eventHandlerWithMetadata = TypedEventMetadataHandler(action: action)
}

init(action: @escaping SplitAction, event: SplitEvent, runInBackground: Bool = false, factory: SplitFactory, queue: DispatchQueue? = nil) {
Expand All @@ -45,7 +41,25 @@ class SplitEventActionTask: SplitEventTask, @unchecked Sendable {
eventHandler?()

if let metadata = metadata {
eventHandlerWithMetadata?(metadata)
eventHandlerWithMetadata?.execute(metadata)
}
}
}

// MARK: This below is used to preserve the concrete type of the event, since the pipeline uses the erased type.
// Type erasure container to preserve concrete type information
private protocol EventMetadataHandler: Sendable {
func execute(_ metadata: EventMetadata)
}

private struct TypedEventMetadataHandler<T: EventMetadata>: EventMetadataHandler {
let action: SplitActionWithMetadata<T>

func execute(_ metadata: EventMetadata) {
guard let typed = metadata as? T else {
Logger.e("Wrong metadata type for event handler. Expected \(T.self), got \(type(of: metadata)).")
return
}
action(typed)
}
}
41 changes: 31 additions & 10 deletions Split/Events/SplitEventsManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -120,40 +120,58 @@ class DefaultSplitEventsManager: SplitEventsManager, @unchecked Sendable {
return isRunning
}

// MARK: Here we map InternalEvents to external Events
private func processEvent(_ event: SplitInternalEventWithMetadata) {
guard isRunning() else { return }

triggered.append(event)
switch event.type {
case .splitsUpdated, .mySegmentsUpdated, .myLargeSegmentsUpdated:

// MARK: NORMAL SDK UPDATE
if isTriggered(external: .sdkReady) {
trigger(event: .sdkUpdated)
trigger(event: SplitEventWithMetadata(type: .sdkUpdated, metadata: event.metadata))
return
}
self.triggerSdkReadyIfNeeded()

// MARK: SDK READY
var lastUpdateTimestamp: Int64?
if event.type == .splitsUpdated, let timestamp = event.extra as? Int64 { // Get timestamp from splitsUpdated metadata
lastUpdateTimestamp = timestamp == 0 ? nil : timestamp
}
triggerSdkReadyIfNeeded(SdkReadyMetadata(lastUpdateTimestamp: lastUpdateTimestamp, isInitialCacheLoad: lastUpdateTimestamp == nil))

case .mySegmentsLoadedFromCache, .myLargeSegmentsLoadedFromCache,
.splitsLoadedFromCache, .attributesLoadedFromCache:
case .mySegmentsLoadedFromCache, .myLargeSegmentsLoadedFromCache, .splitsLoadedFromCache, .attributesLoadedFromCache:

Logger.v("Event \(event) triggered")
if isTriggered(internal: .splitsLoadedFromCache),
isTriggered(internal: .mySegmentsLoadedFromCache),
isTriggered(internal: .myLargeSegmentsLoadedFromCache),
isTriggered(internal: .attributesLoadedFromCache) {
trigger(event: SplitEvent.sdkReadyFromCache)

// MARK: READY FROM CACHE - NOT FRESH INSTALL
var lastUpdateTimestamp: Int64?
if event.type == .splitsLoadedFromCache, let timestamp = event.extra as? Int64 { // Get timestamp from splitsLoaded metadata
lastUpdateTimestamp = timestamp
}

trigger(event: SplitEventWithMetadata(type: .sdkReadyFromCache, metadata: SdkReadyFromCacheMetadata(lastUpdateTimestamp: lastUpdateTimestamp, isInitialCacheLoad: false)))
}
case .splitKilledNotification:
// MARK: KILLED NOTIF (SDK UPDATE)
if isTriggered(external: .sdkReady) {
trigger(event: .sdkUpdated)
trigger(event: SplitEventWithMetadata(type: .sdkUpdated, metadata: event.metadata))
return
}
case .sdkReadyTimeoutReached:
// MARK: TIMEOUT
if !isTriggered(external: .sdkReady) {
trigger(event: SplitEvent.sdkReadyTimedOut)
}
}
}

// MARK: Helper functions.
// MARK: Helper functions
func isTriggered(external event: SplitEvent) -> Bool {
var triggered = false
dataAccessQueue.sync {
Expand All @@ -166,15 +184,18 @@ class DefaultSplitEventsManager: SplitEventsManager, @unchecked Sendable {
return triggered
}

private func triggerSdkReadyIfNeeded() {
private func triggerSdkReadyIfNeeded(_ metadata: SdkReadyMetadata) {
if isTriggered(internal: .mySegmentsUpdated),
isTriggered(internal: .splitsUpdated),
isTriggered(internal: .myLargeSegmentsUpdated),
!isTriggered(external: .sdkReady) {
if !isTriggered(external: .sdkReadyFromCache) {
self.trigger(event: .sdkReadyFromCache)

// MARK: READY FROM CACHE - FRESH INSTALL
trigger(event: SplitEventWithMetadata(type: .sdkReadyFromCache, metadata: SdkReadyFromCacheMetadata(lastUpdateTimestamp: nil, isInitialCacheLoad: true)))
}
self.trigger(event: .sdkReady)

self.trigger(event: SplitEventWithMetadata(type: .sdkReady, metadata: metadata))
}
}

Expand Down
6 changes: 4 additions & 2 deletions Split/Events/SplitInternalEvent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,14 @@ enum SplitInternalEvent {
case splitKilledNotification
}

struct SplitInternalEventWithMetadata {
struct SplitInternalEventWithMetadata: @unchecked Sendable {
let type: SplitInternalEvent
let metadata: EventMetadata?
var extra: Any?

init(_ type: SplitInternalEvent, metadata: EventMetadata? = nil) {
init(_ type: SplitInternalEvent, metadata: EventMetadata? = nil, extra: Any? = nil) {
self.type = type
self.metadata = metadata
self.extra = extra
}
}
27 changes: 21 additions & 6 deletions Split/Events/SplitMetadata.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,15 @@ import Foundation
@objc public enum SdkUpdateMetadataType: Int, Sendable {
case FLAGS_UPDATE
case SEGMENTS_UPDATE

public func toString() -> String {
switch self {
case .FLAGS_UPDATE:
return "FLAGS_UPDATE"
case .SEGMENTS_UPDATE:
return "SEGMENTS_UPDATE"
}
}
}

/// Metadata for SDK update event.
Expand All @@ -29,7 +38,7 @@ import Foundation
/// - The type of update that occurred (flags or segments)
/// - The specific flags affected
///
@objcMembers public final class SdkUpdateMetadata: NSObject, EventMetadata {
@objcMembers public final class SdkUpdateMetadata: NSObject, EventMetadata, Sendable {

public let type: SdkUpdateMetadataType

Expand Down Expand Up @@ -58,12 +67,12 @@ import Foundation
/// - The timestamp of the last successful update.
/// - Whether the data was loaded from the initial cache.
///
@objcMembers public final class SdkReadyMetadata: NSObject, EventMetadata {
@objcMembers public final class SdkReadyMetadata: NSObject, EventMetadata, Sendable {

/// Timestamp (in milliseconds since epoch) of the last successful SDK update.
///
/// A value of `-1` indicates that no update has occurred yet.
public let lastUpdateTimestamp: Int64
public let lastUpdateTimestamp: Int64?

/// Indicates whether this SDK initialization corresponds to a fresh install.
public let isInitialCacheLoad: Bool
Expand All @@ -73,7 +82,7 @@ import Foundation
fatalError("Use SDK-provided instances only")
}

internal init(lastUpdateTimestamp: Int64, isInitialCacheLoad: Bool) {
internal init(lastUpdateTimestamp: Int64? = nil, isInitialCacheLoad: Bool) {
self.isInitialCacheLoad = isInitialCacheLoad
self.lastUpdateTimestamp = lastUpdateTimestamp
super.init()
Expand All @@ -90,7 +99,12 @@ import Foundation
/// - The SDK initialized using previously stored data.
/// - No fresh data has been fetched from the network yet.
///
@objcMembers public final class SdkReadyFromCacheMetadata: NSObject, EventMetadata {
@objcMembers public final class SdkReadyFromCacheMetadata: NSObject, EventMetadata, Sendable {

/// Timestamp (in milliseconds since epoch) of the last successful SDK update.
///
/// A value of `-1` indicates that no update has occurred yet.
public let lastUpdateTimestamp: Int64?

/// Indicates whether this SDK initialization corresponds to a fresh install.
public let isInitialCacheLoad: Bool
Expand All @@ -100,8 +114,9 @@ import Foundation
fatalError("Use SDK-provided instances only")
}

internal init(isInitialCacheLoad: Bool) {
internal init(lastUpdateTimestamp: Int64? = nil, isInitialCacheLoad: Bool) {
self.isInitialCacheLoad = isInitialCacheLoad
self.lastUpdateTimestamp = lastUpdateTimestamp
super.init()
}
}
28 changes: 21 additions & 7 deletions Split/FetcherEngine/Refresh/PeriodicSyncWorker.swift
Original file line number Diff line number Diff line change
Expand Up @@ -130,10 +130,13 @@ class BasePeriodicSyncWorker: PeriodicSyncWorker, @unchecked Sendable {
Logger.i("Fetch from remote not implemented")
}

func notifyUpdate(_ events: [SplitInternalEvent]) {
events.forEach {
eventsManager.notifyInternalEvent($0)
}
func notifyUpdate(_ event: SplitInternalEvent) {
let withMetadata = SplitInternalEventWithMetadata(event, metadata: nil)
notifyUpdate(withMetadata)
}

func notifyUpdate(_ event: SplitInternalEventWithMetadata) {
eventsManager.notifyInternalEvent(event)
}
}

Expand Down Expand Up @@ -183,8 +186,18 @@ class PeriodicSplitsSyncWorker: BasePeriodicSyncWorker, @unchecked Sendable {
guard let result = try? syncHelper.sync(since: changeNumber, rbSince: rbChangeNumber) else {
return
}
if result.success, result.featureFlagsUpdated || result.rbsUpdated {
notifyUpdate([.splitsUpdated])
if result.success {

if !result.featureFlagsUpdated.isEmpty {
let event = SplitInternalEventWithMetadata(.splitsUpdated, metadata: SdkUpdateMetadata(type: .FLAGS_UPDATE, names: result.featureFlagsUpdated))
notifyUpdate(event)
return // Avoid duplicate notification
}

if result.rbsUpdated {
let event = SplitInternalEventWithMetadata(.splitsUpdated, metadata: SdkUpdateMetadata(type: .SEGMENTS_UPDATE, names: []))
notifyUpdate(event)
}
}
}
}
Expand Down Expand Up @@ -226,7 +239,8 @@ class PeriodicMySegmentsSyncWorker: BasePeriodicSyncWorker, @unchecked Sendable
if result.success {
if result.msUpdated || result.mlsUpdated {
// For now is not necessary specify which entity was updated
notifyUpdate([.mySegmentsUpdated])
let event = SplitInternalEventWithMetadata(.mySegmentsUpdated, metadata: SdkUpdateMetadata(type: .SEGMENTS_UPDATE, names: []))
notifyUpdate(event)
}
}
} catch {
Expand Down
9 changes: 6 additions & 3 deletions Split/FetcherEngine/Refresh/RetryableSegmentsSyncWorker.swift
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,14 @@ class RetryableMySegmentsSyncWorker: BaseRetryableSyncWorker, @unchecked Sendabl
if result.success {
if !isSdkReadyTriggered() {
// Notifying both to trigger SDK Ready
notifyUpdate([.mySegmentsUpdated])
notifyUpdate([.myLargeSegmentsUpdated])
var event = SplitInternalEventWithMetadata(.mySegmentsUpdated, metadata: SdkUpdateMetadata(type: .SEGMENTS_UPDATE, names: []))
notifyUpdate(event)
event = SplitInternalEventWithMetadata(.myLargeSegmentsUpdated, metadata: SdkUpdateMetadata(type: .SEGMENTS_UPDATE, names: []))
notifyUpdate(event)
} else if result.msUpdated || result.mlsUpdated {
// For now is not necessary specify which entity was updated
notifyUpdate([.mySegmentsUpdated])
let event = SplitInternalEventWithMetadata(.mySegmentsUpdated, metadata: SdkUpdateMetadata(type: .SEGMENTS_UPDATE, names: []))
notifyUpdate(event)
}
return true
}
Expand Down
Loading
Loading