Skip to content

Commit 8ff78dd

Browse files
authored
Merge branch 'master' into fix/SDK-412-UUA-Naming-inconsistencies
2 parents 0af342f + 40f2de1 commit 8ff78dd

12 files changed

Lines changed: 726 additions & 68 deletions

agent_test.sh

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,21 @@ fi
5757

5858
echo "Running Iterable Swift SDK unit tests..."
5959

60+
# Resolve the simulator by name and boot it via CoreSimulator. Targeting by
61+
# UDID avoids xcodebuild's "no matching device" failures when multiple devices
62+
# share the same name across runtimes. This does NOT touch any Simulator.app
63+
# instance the user may already have open for another project.
64+
SIMULATOR_NAME="${SIMULATOR_NAME:-iPhone 17 Pro}"
65+
SIMULATOR_ID=$(xcrun simctl list devices available | grep -F "$SIMULATOR_NAME (" | head -1 | sed -E 's/.*\(([A-F0-9-]+)\).*/\1/')
66+
67+
if [[ -z "$SIMULATOR_ID" ]]; then
68+
echo "❌ Could not find available simulator named '$SIMULATOR_NAME'"
69+
exit 1
70+
fi
71+
72+
xcrun simctl boot "$SIMULATOR_ID" >/dev/null 2>&1 || true
73+
xcrun simctl bootstatus "$SIMULATOR_ID" -b >/dev/null
74+
6075
# Create a temporary file for the test output
6176
TEMP_OUTPUT=$(mktemp)
6277

@@ -65,7 +80,7 @@ XCODEBUILD_CMD="xcodebuild test \
6580
-project swift-sdk.xcodeproj \
6681
-scheme swift-sdk \
6782
-sdk iphonesimulator \
68-
-destination 'platform=iOS Simulator,name=iPhone 17 Pro' \
83+
-destination 'platform=iOS Simulator,id=$SIMULATOR_ID' \
6984
-enableCodeCoverage YES \
7085
-skipPackagePluginValidation \
7186
CODE_SIGNING_REQUIRED=NO"
@@ -124,4 +139,4 @@ fi
124139
# Remove the temporary file
125140
rm $TEMP_OUTPUT
126141

127-
exit $FINAL_STATUS
142+
exit $FINAL_STATUS

swift-sdk.xcodeproj/project.pbxproj

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -405,7 +405,9 @@
405405
ACDBB33B239582450036BB38 /* NotificationExtensionConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = ACDBB33A239582450036BB38 /* NotificationExtensionConstants.swift */; };
406406
ACE34AB5213776CB00691224 /* LocalStorageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ACE34AB4213776CB00691224 /* LocalStorageTests.swift */; };
407407
ACED4C01213F50B30055A497 /* LoggingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ACED4C00213F50B30055A497 /* LoggingTests.swift */; };
408+
8A4680022E4D000100000001 /* NetworkMonitorLifecycleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A4680012E4D000100000001 /* NetworkMonitorLifecycleTests.swift */; };
408409
ACEDF41F2183C436000B9BFE /* PendingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ACEDF41E2183C436000B9BFE /* PendingTests.swift */; };
410+
5C0471F221F0F0F0000B7CCC /* PendingConcurrencyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C0471F121F0F0F0000B7CCC /* PendingConcurrencyTests.swift */; };
409411
ACF406252507F90F005FD775 /* NetworkConnectivityManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ACF406242507F90F005FD775 /* NetworkConnectivityManagerTests.swift */; };
410412
ACF560D620E443BF000AAC23 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = ACF560D520E443BF000AAC23 /* AppDelegate.swift */; };
411413
ACF560DB20E443BF000AAC23 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = ACF560D920E443BF000AAC23 /* Main.storyboard */; };
@@ -851,7 +853,9 @@
851853
ACDBB33A239582450036BB38 /* NotificationExtensionConstants.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NotificationExtensionConstants.swift; sourceTree = "<group>"; };
852854
ACE34AB4213776CB00691224 /* LocalStorageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalStorageTests.swift; sourceTree = "<group>"; };
853855
ACED4C00213F50B30055A497 /* LoggingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoggingTests.swift; sourceTree = "<group>"; };
856+
8A4680012E4D000100000001 /* NetworkMonitorLifecycleTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkMonitorLifecycleTests.swift; sourceTree = "<group>"; };
854857
ACEDF41E2183C436000B9BFE /* PendingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PendingTests.swift; sourceTree = "<group>"; };
858+
5C0471F121F0F0F0000B7CCC /* PendingConcurrencyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PendingConcurrencyTests.swift; sourceTree = "<group>"; };
855859
ACF406242507F90F005FD775 /* NetworkConnectivityManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkConnectivityManagerTests.swift; sourceTree = "<group>"; };
856860
ACF560D320E443BF000AAC23 /* host-app.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "host-app.app"; sourceTree = BUILT_PRODUCTS_DIR; };
857861
ACF560D520E443BF000AAC23 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
@@ -1373,8 +1377,10 @@
13731377
55B37FC0229620D20042F13A /* CommerceItemTests.swift */,
13741378
5536781E2576FF9000DB3652 /* IterableUtilTests.swift */,
13751379
ACED4C00213F50B30055A497 /* LoggingTests.swift */,
1380+
8A4680012E4D000100000001 /* NetworkMonitorLifecycleTests.swift */,
13761381
55B37FC5229752DD0042F13A /* OrderedDictionaryTests.swift */,
13771382
ACEDF41E2183C436000B9BFE /* PendingTests.swift */,
1383+
5C0471F121F0F0F0000B7CCC /* PendingConcurrencyTests.swift */,
13781384
);
13791385
name = "foundational-tests";
13801386
sourceTree = "<group>";
@@ -2410,6 +2416,7 @@
24102416
5588DFE928C046D7000697D7 /* MockInboxState.swift in Sources */,
24112417
181063DF2C9D51000078E0ED /* ValidateStoredEventCheckUnknownToKnownUserTest.swift in Sources */,
24122418
ACED4C01213F50B30055A497 /* LoggingTests.swift in Sources */,
2419+
8A4680022E4D000100000001 /* NetworkMonitorLifecycleTests.swift in Sources */,
24132420
AC52C5B8272A8B32000DCDCF /* KeychainWrapperTests.swift in Sources */,
24142421
ACC3FD9E2536D7A30004A2E0 /* InAppFilePersistenceTests.swift in Sources */,
24152422
5B5AA714284F1A6D0093FED4 /* MockNetworkSession.swift in Sources */,
@@ -2465,6 +2472,7 @@
24652472
5588DFA128C04570000697D7 /* MockApplicationStateProvider.swift in Sources */,
24662473
5588DFF128C046FF000697D7 /* MockMessageViewControllerEventTracker.swift in Sources */,
24672474
ACEDF41F2183C436000B9BFE /* PendingTests.swift in Sources */,
2475+
5C0471F221F0F0F0000B7CCC /* PendingConcurrencyTests.swift in Sources */,
24682476
AC1B29002742579000AD2BE3 /* InAppNavigationTests.swift in Sources */,
24692477
5588DF7128C0442D000697D7 /* MockDateProvider.swift in Sources */,
24702478
1CBFFE1A2A97AEEF00ED57EE /* EmbeddedManagerTests.swift in Sources */,

swift-sdk/Internal/Network/NetworkConnectivityManager.swift

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,12 +48,14 @@ class NetworkConnectivityManager: NSObject {
4848

4949
func stop() {
5050
ITBInfo()
51+
networkMonitor.statusUpdatedCallback = nil
5152
networkMonitor.stop()
5253
stopTimer()
5354
}
5455

5556
private func startTimer() {
5657
ITBInfo()
58+
stopTimer()
5759
let interval = online ? onlineModePollingInterval : offlineModePollingInterval
5860
timer = Timer.scheduledTimer(withTimeInterval: interval, repeats: true, block: { [weak self] _ in
5961
ITBInfo("timer called")
@@ -75,8 +77,8 @@ class NetworkConnectivityManager: NSObject {
7577

7678
private func updateStatus() {
7779
ITBInfo()
78-
connectivityChecker.checkConnectivity().onSuccess { connected in
79-
self.online = connected
80+
connectivityChecker.checkConnectivity().onSuccess { [weak self] connected in
81+
self?.online = connected
8082
}
8183
}
8284

@@ -116,4 +118,3 @@ class NetworkConnectivityManager: NSObject {
116118
}
117119
}
118120
}
119-

swift-sdk/Internal/Network/NetworkMonitor.swift

Lines changed: 53 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,45 @@ import Foundation
88
import Network
99
#endif
1010

11+
struct NetworkPathUpdate {
12+
let debugDescription: String
13+
let status: String
14+
}
15+
16+
protocol NetworkPathMonitorProtocol: AnyObject {
17+
var pathUpdateHandler: ((NetworkPathUpdate) -> Void)? { get set }
18+
func start(queue: DispatchQueue)
19+
func cancel()
20+
}
21+
22+
#if canImport(Network)
23+
private final class NetworkPathMonitor: NetworkPathMonitorProtocol {
24+
var pathUpdateHandler: ((NetworkPathUpdate) -> Void)? {
25+
didSet {
26+
guard let pathUpdateHandler = pathUpdateHandler else {
27+
monitor.pathUpdateHandler = nil
28+
return
29+
}
30+
31+
monitor.pathUpdateHandler = { path in
32+
pathUpdateHandler(NetworkPathUpdate(debugDescription: path.debugDescription,
33+
status: String(describing: path.status)))
34+
}
35+
}
36+
}
37+
38+
func start(queue: DispatchQueue) {
39+
monitor.start(queue: queue)
40+
}
41+
42+
func cancel() {
43+
monitor.cancel()
44+
}
45+
46+
private let monitor = NWPathMonitor()
47+
}
48+
#endif
49+
1150
/// Listens to network interface to detect status change.
1251
/// It only knows that the status has changed.
1352
/// It does not know if the network is online or not.
@@ -18,8 +57,9 @@ protocol NetworkMonitorProtocol {
1857
}
1958

2059
class NetworkMonitor: NetworkMonitorProtocol {
21-
init() {
60+
init(pathMonitorFactory: @escaping () -> NetworkPathMonitorProtocol = { NetworkPathMonitor() }) {
2261
ITBInfo()
62+
self.pathMonitorFactory = pathMonitorFactory
2363
}
2464

2565
deinit {
@@ -31,10 +71,12 @@ class NetworkMonitor: NetworkMonitorProtocol {
3171

3272
func start() {
3373
ITBInfo()
34-
let networkMonitor = NWPathMonitor()
35-
networkMonitor.pathUpdateHandler = { path in
74+
stop()
75+
76+
let networkMonitor = pathMonitorFactory()
77+
networkMonitor.pathUpdateHandler = { [weak self] path in
3678
ITBInfo("networkMonitor.pathUpdateHandler, path: \(path.debugDescription), status: \(path.status)")
37-
self.statusUpdatedCallback?()
79+
self?.statusUpdatedCallback?()
3880
}
3981

4082
networkMonitor.start(queue: queue)
@@ -43,10 +85,16 @@ class NetworkMonitor: NetworkMonitorProtocol {
4385

4486
func stop() {
4587
ITBInfo()
88+
// `statusUpdatedCallback` intentionally persists across stop/start cycles:
89+
// callers (e.g. `NetworkConnectivityManager`) set it once and expect it to
90+
// survive, so they don't have to re-register after every stop. The
91+
// `pathUpdateHandler` weak-self capture means this is not a leak risk.
92+
networkMonitor?.pathUpdateHandler = nil
4693
networkMonitor?.cancel()
4794
networkMonitor = nil
4895
}
4996

50-
private weak var networkMonitor: NWPathMonitor?
97+
private var networkMonitor: NetworkPathMonitorProtocol?
98+
private let pathMonitorFactory: () -> NetworkPathMonitorProtocol
5199
private let queue = DispatchQueue(label: "NetworkMonitor")
52100
}

swift-sdk/Internal/Pending.swift

Lines changed: 71 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -21,78 +21,98 @@ extension IterableError: LocalizedError {
2121
// either there is a success with result
2222
// or there is a failure with error
2323
// There is no way to set value a result in this class.
24+
//
25+
// Thread-safety: all mutations of `result`, `successCallbacks`, and `errorCallbacks`
26+
// are serialized through `stateQueue`. Callbacks are snapshotted under `.sync` and
27+
// invoked outside the critical section, so user code that re-enters `onSuccess` /
28+
// `onError` on the same instance cannot deadlock the queue.
2429
public class Pending<Value, Failure> where Failure: Error {
2530
fileprivate var successCallbacks = [(Value) -> Void]()
2631
fileprivate var errorCallbacks = [(Failure) -> Void]()
27-
32+
fileprivate var result: Result<Value, Failure>?
33+
34+
private let stateQueue = DispatchQueue(label: "com.iterable.pending.state")
35+
2836
public func onCompletion(receiveValue: @escaping ((Value) -> Void), receiveError: ( (Failure) -> Void)? = nil) {
29-
successCallbacks.append(receiveValue)
30-
31-
// if a successful result already exists (from constructor), report it
32-
if case let Result.success(value)? = result {
33-
successCallbacks.forEach { $0(value) }
34-
}
35-
36-
if let receiveError = receiveError {
37-
errorCallbacks.append(receiveError)
38-
39-
// if a failed result already exists (from constructor), report it
40-
if case let Result.failure(error)? = result {
41-
errorCallbacks.forEach { $0(error) }
37+
// Append both callbacks in one critical section so a concurrent failure
38+
// cannot land between the success and error registrations.
39+
let current: Result<Value, Failure>? = stateQueue.sync {
40+
successCallbacks.append(receiveValue)
41+
if let receiveError = receiveError {
42+
errorCallbacks.append(receiveError)
4243
}
44+
return result
45+
}
46+
47+
// Late registration on an already-resolved Pending: replay the current
48+
// result for the newly registered callback only.
49+
guard let current = current else { return }
50+
switch current {
51+
case let .success(value):
52+
receiveValue(value)
53+
case let .failure(error):
54+
receiveError?(error)
4355
}
4456
}
45-
57+
4658
@discardableResult public func onSuccess(block: @escaping ((Value) -> Void)) -> Pending<Value, Failure> {
47-
successCallbacks.append(block)
48-
49-
// if a successful result already exists (from constructor), report it
50-
if case let Result.success(value)? = result {
51-
successCallbacks.forEach { $0(value) }
59+
let current: Result<Value, Failure>? = stateQueue.sync {
60+
successCallbacks.append(block)
61+
return result
62+
}
63+
64+
if case let .success(value)? = current {
65+
block(value)
5266
}
53-
5467
return self
5568
}
56-
69+
5770
@discardableResult public func onError(block: @escaping ((Failure) -> Void)) -> Pending<Value, Failure> {
58-
errorCallbacks.append(block)
59-
60-
// if a failed result already exists (from constructor), report it
61-
if case let Result.failure(error)? = result {
62-
errorCallbacks.forEach { $0(error) }
71+
let current: Result<Value, Failure>? = stateQueue.sync {
72+
errorCallbacks.append(block)
73+
return result
74+
}
75+
76+
if case let .failure(error)? = current {
77+
block(error)
6378
}
64-
6579
return self
6680
}
67-
81+
6882
public func isResolved() -> Bool {
69-
result != nil
83+
stateQueue.sync { result != nil }
7084
}
71-
85+
7286
public func wait() {
7387
ITBDebug()
7488
guard !isResolved() else {
7589
ITBDebug("isResolved")
7690
return
7791
}
78-
92+
7993
ITBDebug("waiting....")
8094
Thread.sleep(forTimeInterval: 0.1)
8195
wait()
8296
}
83-
84-
fileprivate var result: Result<Value, Failure>? {
85-
// Observe whenever a result is assigned, and report it
86-
didSet { result.map(report) }
87-
}
88-
89-
// Report success or error based on result
90-
private func report(result: Result<Value, Failure>) {
91-
switch result {
97+
98+
/// Stores the result and fires every currently-registered callback.
99+
///
100+
/// Repeated calls are allowed: in-tree callers reuse a single `Fulfill` as a
101+
/// broadcast event signal. Each call overwrites `result` and fires the matching
102+
/// branch against a snapshot of the callback list taken under the lock.
103+
/// Callbacks fire outside the critical section so re-entrant registrations on
104+
/// the same instance cannot deadlock.
105+
fileprivate func setResult(_ newResult: Result<Value, Failure>) {
106+
let snapshot: (successes: [(Value) -> Void], errors: [(Failure) -> Void]) = stateQueue.sync {
107+
result = newResult
108+
return (successCallbacks, errorCallbacks)
109+
}
110+
111+
switch newResult {
92112
case let .success(value):
93-
successCallbacks.forEach { $0(value) }
113+
snapshot.successes.forEach { $0(value) }
94114
case let .failure(error):
95-
errorCallbacks.forEach { $0(error) }
115+
snapshot.errors.forEach { $0(error) }
96116
}
97117
}
98118
}
@@ -101,7 +121,7 @@ public class Pending<Value, Failure> where Failure: Error {
101121
public class FailPending<Value, Failure: Error>: Pending<Value, Failure> {
102122
public init(error: Failure) {
103123
super.init()
104-
self.result = .failure(error)
124+
setResult(.failure(error))
105125
}
106126
}
107127

@@ -199,27 +219,25 @@ public class Fulfill<Value, Failure>: Pending<Value, Failure> where Failure: Err
199219
ITBDebug()
200220
super.init()
201221
if let value = value {
202-
result = Result.success(value)
203-
} else {
204-
result = nil
222+
setResult(.success(value))
205223
}
206224
}
207-
225+
208226
public init(error: Failure) {
209227
ITBDebug()
210228
super.init()
211-
result = Result.failure(error)
229+
setResult(.failure(error))
212230
}
213231

214232
deinit {
215233
ITBDebug()
216234
}
217-
235+
218236
public func resolve(with value: Value) {
219-
result = .success(value)
237+
setResult(.success(value))
220238
}
221-
239+
222240
public func reject(with error: Failure) {
223-
result = .failure(error)
241+
setResult(.failure(error))
224242
}
225243
}

0 commit comments

Comments
 (0)