Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
d51d885
Added Synchronized primitive and its associated tests. Added AsyncOpe…
Dec 23, 2025
aadb0b9
Removed uses of LockManager in AutoTrackingScreenViews and PushHistor…
Dec 23, 2025
6f80ef1
Removed LockManager and Lock and replaced their uses of NSRecursiveLock.
Dec 26, 2025
80561f8
Removed outdated comment.
Dec 26, 2025
76b1783
Fixed linting.
Dec 26, 2025
66fd950
Whitespace only change for linting.
Dec 26, 2025
c2f720f
Updated API declarations to remove LockManager and Lock while adding …
Dec 26, 2025
528ca50
Added additional test coverage to Synchronized. Fixed spelling mistak…
Dec 26, 2025
a20206c
Removed barrier flag from Synchronized.usingAsync.
Dec 27, 2025
cc2be70
Removed unnecessary ArrayExtensions and the associated tests because …
Dec 27, 2025
6c2b61e
Removed AutoLenses and all autogenerated code.
Dec 28, 2025
afdb118
Updated API docs.
Dec 28, 2025
e4d4195
Minor cleanups in RingBuffer and added additional tests to it for fur…
Dec 28, 2025
57fd683
Removed use of @Atomic from QueueInventoryMemoryStore.
Dec 28, 2025
6c80e2d
Updated Scanfile to use iPhone 16 for testing now since it is the new…
Dec 29, 2025
590a0ea
Removed all SynchronizedTests that involve multiple threads for testi…
Dec 29, 2025
6c628b2
Updated all GitHub actions to use MacOS-15
Dec 29, 2025
f2b1e33
Removed references to AutoLenses.generated.swift from APN Test Project.
Dec 30, 2025
68b9f95
Fixed Linting
Dec 30, 2025
850eb74
Rewrote AtomicTests to prevent them from failing intermittently while…
Dec 30, 2025
66672db
Added some sanity checks to Synchronized and Enabled a few of the dis…
Dec 30, 2025
fd2b22b
Enabled remaining Synchronized Tests and ran Linting.
Dec 30, 2025
ccef5d4
Removed tests that were likely causes of deadlock.
Dec 30, 2025
c345ada
Enabled usingDetatched test to see if that causes the issue.
Dec 30, 2025
999976c
Enabled mutatingDetatched test.
Dec 30, 2025
7332a54
Removed Detatched functionality from Synchronized and it's associated…
Dec 30, 2025
b03581b
Fixed AsyncOperation to address cancelation race condition raised by …
Dec 30, 2025
869a746
Fixed linting.
Dec 30, 2025
845f249
Converted AsyncOperation to use a lock rather than a queue.
Dec 30, 2025
c5612c1
Revert "Converted AsyncOperation to use a lock rather than a queue."
Dec 30, 2025
97c2700
Revert "Fixed linting."
Dec 30, 2025
6c17fc3
Revert "Fixed AsyncOperation to address cancelation race condition ra…
Dec 30, 2025
f8e6f93
Added additional check for cancelation before starting the task on As…
Dec 31, 2025
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/build-sample-apps.yml
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ jobs:

# 3) Generate SDK size reports or other tasks
generate-sdk-size-reports:
runs-on: macos-14
runs-on: macos-15
permissions:
pull-requests: write
steps:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/check-api-breaking-changes.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ concurrency: # cancel previous workflow run if one exists

jobs:
check-api-breaking-changes:
runs-on: macos-14
runs-on: macos-15
steps:
- uses: actions/checkout@v4

Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/deploy-sdk.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ jobs:
# Therefore, generating SDK reports is a separate job that runs before deployment.
generate-sdk-size-report:
name: Generate SDK size report to attach to the release
runs-on: macos-14
runs-on: macos-15
# In order to pass data from 1 action to another, you use outputs.
# These are the generated reports that the deployment action depends on.
outputs:
Expand Down Expand Up @@ -178,7 +178,7 @@ jobs:
if: ${{ needs.deploy-git-tag.outputs.new_release_published == 'true' || github.event_name == 'workflow_dispatch' }}
env:
COCOAPODS_TRUNK_TOKEN: ${{ secrets.COCOAPODS_TRUNK_TOKEN }}
runs-on: macos-14
runs-on: macos-15
steps:
- name: Checkout git tag that got created in previous step
uses: actions/checkout@v4
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/reusable_build_sample_apps.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ on:

jobs:
build_sample_apps:
runs-on: macos-14
runs-on: macos-15
name: Building iOS sample apps
strategy:
fail-fast: false
Expand Down
4 changes: 0 additions & 4 deletions Apps/APN-UIKit/APN UIKit.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
30F1170A2D6356BA00DB1E11 /* SampleAppsCommon in Frameworks */ = {isa = PBXBuildFile; productRef = 30F117092D6356BA00DB1E11 /* SampleAppsCommon */; };
4650330629F68FEB001B6552 /* NotificationServiceExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 465032FF29F68FEB001B6552 /* NotificationServiceExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
669B9FA92D8B2D430087CC20 /* AutoDependencyInjection.generated.swift in Sources */ = {isa = PBXBuildFile; fileRef = 669B9F6E2D8B2D430087CC20 /* AutoDependencyInjection.generated.swift */; };
669B9FAA2D8B2D430087CC20 /* AutoLenses.generated.swift in Sources */ = {isa = PBXBuildFile; fileRef = 669B9F6F2D8B2D430087CC20 /* AutoLenses.generated.swift */; };
669B9FAB2D8B2D430087CC20 /* AutoMockable.generated.swift in Sources */ = {isa = PBXBuildFile; fileRef = 669B9F702D8B2D430087CC20 /* AutoMockable.generated.swift */; };
669B9FAC2D8B2D430087CC20 /* DIDependencies.swift in Sources */ = {isa = PBXBuildFile; fileRef = 669B9F712D8B2D430087CC20 /* DIDependencies.swift */; };
669B9FAD2D8B2D430087CC20 /* Settings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 669B9F732D8B2D430087CC20 /* Settings.swift */; };
Expand Down Expand Up @@ -104,7 +103,6 @@
46D5D99329E459D800EAF40B /* APN UIKitTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "APN UIKitTests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; };
46D5D99D29E459D800EAF40B /* APN UIKitUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "APN UIKitUITests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; };
669B9F6E2D8B2D430087CC20 /* AutoDependencyInjection.generated.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutoDependencyInjection.generated.swift; sourceTree = "<group>"; };
669B9F6F2D8B2D430087CC20 /* AutoLenses.generated.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutoLenses.generated.swift; sourceTree = "<group>"; };
669B9F702D8B2D430087CC20 /* AutoMockable.generated.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutoMockable.generated.swift; sourceTree = "<group>"; };
669B9F712D8B2D430087CC20 /* DIDependencies.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DIDependencies.swift; sourceTree = "<group>"; };
669B9F732D8B2D430087CC20 /* Settings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Settings.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -231,7 +229,6 @@
isa = PBXGroup;
children = (
669B9F6E2D8B2D430087CC20 /* AutoDependencyInjection.generated.swift */,
669B9F6F2D8B2D430087CC20 /* AutoLenses.generated.swift */,
669B9F702D8B2D430087CC20 /* AutoMockable.generated.swift */,
669B9F712D8B2D430087CC20 /* DIDependencies.swift */,
);
Expand Down Expand Up @@ -574,7 +571,6 @@
buildActionMask = 2147483647;
files = (
669B9FA92D8B2D430087CC20 /* AutoDependencyInjection.generated.swift in Sources */,
669B9FAA2D8B2D430087CC20 /* AutoLenses.generated.swift in Sources */,
669B9FAB2D8B2D430087CC20 /* AutoMockable.generated.swift in Sources */,
669B9FAC2D8B2D430087CC20 /* DIDependencies.swift in Sources */,
669B9FAD2D8B2D430087CC20 /* Settings.swift in Sources */,
Expand Down

This file was deleted.

4 changes: 1 addition & 3 deletions Sources/Common/Background Queue/QueueStorage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,11 @@ public class FileManagerQueueStorage: QueueStorage {
let dateUtil: DateUtil
private var inventoryStore: QueueInventoryMemoryStore

let lock: Lock
let lock = NSRecursiveLock()

init(
fileStorage: FileStorage,
jsonAdapter: JsonAdapter,
lockManager: LockManager,
logger: Logger,
dateUtil: DateUtil,
inventoryStore: QueueInventoryMemoryStore
Expand All @@ -51,7 +50,6 @@ public class FileManagerQueueStorage: QueueStorage {
self.jsonAdapter = jsonAdapter
self.logger = logger
self.dateUtil = dateUtil
self.lock = lockManager.getLock(id: .queueStorage)
self.inventoryStore = inventoryStore
}

Expand Down
10 changes: 5 additions & 5 deletions Sources/Common/Background Queue/Type/QueueTask.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,15 +38,15 @@ import Foundation
headers: nil, body: taskData.attributesJsonString?.data)
```
*/
public struct QueueTask: Codable, AutoLenses, Equatable {
public struct QueueTask: Codable, Equatable {
/// ID used to store the task in persistant storage
public let storageId: String
public var storageId: String
/// the type of task. used when running tasks
public let type: String
public var type: String
/// data required to run the task
public let data: Data
public var data: Data
/// the current run results of the task. keeping track of the history of the task
public let runResults: QueueTaskRunResults
public var runResults: QueueTaskRunResults

enum CodingKeys: String, CodingKey {
case storageId = "storage_id"
Expand Down
14 changes: 7 additions & 7 deletions Sources/Common/Background Queue/Type/QueueTaskMetadata.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,17 @@ import Foundation
/// Pointer to full queue task in persistent storage.
/// This data structure is meant to be as small as possible with the
/// ability to hold all queue task metadata in memory at runtime.
public struct QueueTaskMetadata: Codable, Equatable, Hashable, AutoLenses {
let taskPersistedId: String
let taskType: String
public struct QueueTaskMetadata: Codable, Equatable, Hashable {
public var taskPersistedId: String
public var taskType: String
/// The start of a new group of tasks.
/// Tasks can be the start of of 0 or 1 groups
let groupStart: String?
public var groupStart: String?
/// Groups that this task belongs to.
/// Tasks can belong to 0+ groups
let groupMember: [String]?
public var groupMember: [String]?
/// Populated when the task is added to the queue.
let createdAt: Date
public var createdAt: Date

enum CodingKeys: String, CodingKey {
case taskPersistedId = "task_persisted_id"
Expand All @@ -24,7 +24,7 @@ public struct QueueTaskMetadata: Codable, Equatable, Hashable, AutoLenses {
}
}

extension QueueTaskMetadata {
public extension QueueTaskMetadata {
static var random: QueueTaskMetadata {
QueueTaskMetadata(
taskPersistedId: String.random,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import Foundation

public struct QueueTaskRunResults: Codable, AutoLenses, Equatable {
let totalRuns: Int
public struct QueueTaskRunResults: Codable, Equatable {
public var totalRuns: Int

enum CodingKeys: String, CodingKey {
case totalRuns = "total_runs"
Expand Down
77 changes: 77 additions & 0 deletions Sources/Common/Communication/AsyncOperation.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import Foundation

extension OperationQueue {
func addAsyncOperation(asyncBlock: @escaping () async -> Void) {
addOperation(AsyncOperation(asyncBlock: asyncBlock))
}
}

public final class AsyncOperation: Operation, @unchecked Sendable {
private let activeTask: Synchronized<Task<Void, Never>?> = .init(initial: nil)

public let asyncBlock: () async -> Void

public init(asyncBlock: @escaping () async -> Void) {
self.asyncBlock = asyncBlock
}

// Only required when you want to manually start an operation
// Ignored when an operation is added to a queue.
override public var isAsynchronous: Bool { true }

// State is accessed and modified in a thread safe and KVO compliant way.
private let _isExecuting: Synchronized<Bool> = .init(initial: false)
override public private(set) var isExecuting: Bool {
get {
_isExecuting.wrappedValue
}
set {
willChangeValue(forKey: "isExecuting")
_isExecuting.wrappedValue = newValue
didChangeValue(forKey: "isExecuting")
}
}

private let _isFinished: Synchronized<Bool> = .init(initial: false)
override public private(set) var isFinished: Bool {
get {
_isFinished.wrappedValue
}
set {
willChangeValue(forKey: "isFinished")
_isFinished.wrappedValue = newValue
didChangeValue(forKey: "isFinished")
}
}

override public func start() {
guard !isCancelled else {
finish()
return
}
isFinished = false
isExecuting = true
main()
}

override public func main() {
activeTask.wrappedValue = Task { [weak self] in
guard let self, !isCancelled else { return }
await asyncBlock()
finish()
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AsyncOperation cancellation leaves operation in incomplete state

When AsyncOperation is cancelled after start() is called but before the Task body executes, the guard statement at line 59 returns early without calling finish(). Since start() already set isExecuting = true, the operation remains in an invalid state (isExecuting = true, isFinished = false) indefinitely. This can cause OperationQueue to hang waiting for operations that will never complete. The issue is particularly relevant in CioEventBusHandler.deinit which calls cancelAllOperations() on pending operations.

Fix in Cursor Fix in Web


override public func cancel() {
activeTask.mutating { value in
value?.cancel()
value = nil
}
super.cancel()
}

func finish() {
isExecuting = false
isFinished = true
}
}
57 changes: 34 additions & 23 deletions Sources/Common/Communication/EventBus.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,18 +40,39 @@ class EventBusObserversHolder {
/// NotificationCenter instance used for observer management.
let notificationCenter: NotificationCenter = .default

/// Dictionary holding arrays of observer tokens for each event type.
/// The keys are event types (as String), and the values are arrays of NotificationCenter tokens.
var observers: [String: [NSObjectProtocol]] = [:]
private let _observers: Synchronized<[String: [NSObjectProtocol]]> = .init(initial: [:])

func storeObserver(_ observer: NSObjectProtocol, for eventKey: String) {
let eventKey = String(describing: eventKey)
_observers.mutating { value in
if value[eventKey] == nil {
value[eventKey] = []
}
value[eventKey]!.append(observer)
}
}

func hasObservers(for eventKey: String) -> Bool {
_observers.wrappedValue[eventKey, default: []].isEmpty == false
}

func removeReservers(for eventKey: String) {
_observers.mutating { value in
value[eventKey]?.forEach(notificationCenter.removeObserver)
value[eventKey] = nil
}
}

/// Removes all observers from the EventBus.
///
/// This function is used for cleanup or resetting the event handling system.
func removeAllObservers() {
observers.forEach { _, observerList in
observerList.forEach(notificationCenter.removeObserver)
_observers.mutating { value in
value.forEach { _, list in
list.forEach(notificationCenter.removeObserver)
}
value.removeAll()
}
observers.removeAll()
}

/// Deinitializer for EventBusObserversHolder.
Expand Down Expand Up @@ -85,7 +106,7 @@ actor SharedEventBus: EventBus {
@discardableResult
func post(_ event: AnyEventRepresentable) -> Bool {
let key = event.key
if let observerList = holder.observers[key], !observerList.isEmpty {
if holder.hasObservers(for: key) {
holder.notificationCenter.post(name: NSNotification.Name(key), object: event)
return true
}
Expand All @@ -97,29 +118,19 @@ actor SharedEventBus: EventBus {
/// - Parameters:
/// - eventType: The type of the event to observe.
/// - action: The action to be executed when the event is received.
func addObserver(_ eventType: String, action: @escaping (AnyEventRepresentable) -> Void) async {
let observer = holder.notificationCenter.addObserver(forName: NSNotification.Name(eventType), object: nil, queue: nil) { notification in
func addObserver(_ eventKey: String, action: @escaping (AnyEventRepresentable) -> Void) async {
let observer = holder.notificationCenter.addObserver(forName: NSNotification.Name(eventKey), object: nil, queue: nil) { notification in
if let event = notification.object as? AnyEventRepresentable {
action(event)
}
}
// Store the observer reference for later management.
if holder.observers[eventType] != nil {
holder.observers[eventType]?.append(observer)
} else {
holder.observers[eventType] = [observer]
}
holder.storeObserver(observer, for: eventKey)
}

/// Removes all observers for a specific event type.
///
/// - Parameter eventType: The event type for which to remove all observers.
func removeObserver(for eventType: String) {
if let observerList = holder.observers[eventType] {
for observer in observerList {
holder.notificationCenter.removeObserver(observer)
}
holder.observers[eventType] = nil
}
/// - Parameter eventKey: The event type for which to remove all observers.
func removeObserver(for eventKey: String) {
holder.removeReservers(for: eventKey)
}
}
Loading
Loading