Skip to content

Commit 28a0365

Browse files
authored
Merge branch 'version-6.5.8-beta' into feature/fix-persistent-container-loading
2 parents d3a9433 + d5dd27a commit 28a0365

File tree

6 files changed

+162
-14
lines changed

6 files changed

+162
-14
lines changed

swift-sdk.xcodeproj/project.pbxproj

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,8 @@
185185
5B5AA717284F1A6D0093FED4 /* MockNetworkSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B5AA710284F1A6D0093FED4 /* MockNetworkSession.swift */; };
186186
5B6C3C1127CE871F00B9A753 /* NavInboxSessionUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B6C3C1027CE871F00B9A753 /* NavInboxSessionUITests.swift */; };
187187
5B88BC482805D09D004016E5 /* NetworkSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B88BC472805D09D004016E5 /* NetworkSession.swift */; };
188+
94D8F9D42CDC291000D4CF53 /* ThreadSafeOrderedDictionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94D8F9D32CDC291000D4CF53 /* ThreadSafeOrderedDictionary.swift */; };
189+
94D8F9D62CDC294300D4CF53 /* ThreadSafeOrderedDictionaryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94D8F9D52CDC294300D4CF53 /* ThreadSafeOrderedDictionaryTests.swift */; };
188190
9F76FFFF2B17884900962526 /* EmbeddedHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F76FFFE2B17884900962526 /* EmbeddedHelper.swift */; };
189191
9FF05EAC2AFEA5FA005311F7 /* MockAuthManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FF05EAB2AFEA5FA005311F7 /* MockAuthManager.swift */; };
190192
9FF05EAD2AFEA5FA005311F7 /* MockAuthManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FF05EAB2AFEA5FA005311F7 /* MockAuthManager.swift */; };
@@ -609,6 +611,8 @@
609611
5B6C3C1027CE871F00B9A753 /* NavInboxSessionUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavInboxSessionUITests.swift; sourceTree = "<group>"; };
610612
5B88BC472805D09D004016E5 /* NetworkSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkSession.swift; sourceTree = "<group>"; };
611613
5BFC7CED27FC9AF300E77479 /* inbox-ui-tests-app.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "inbox-ui-tests-app.entitlements"; sourceTree = "<group>"; };
614+
94D8F9D32CDC291000D4CF53 /* ThreadSafeOrderedDictionary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadSafeOrderedDictionary.swift; sourceTree = "<group>"; };
615+
94D8F9D52CDC294300D4CF53 /* ThreadSafeOrderedDictionaryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadSafeOrderedDictionaryTests.swift; sourceTree = "<group>"; };
612616
9F76FFFE2B17884900962526 /* EmbeddedHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmbeddedHelper.swift; sourceTree = "<group>"; };
613617
9FF05EAB2AFEA5FA005311F7 /* MockAuthManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockAuthManager.swift; sourceTree = "<group>"; };
614618
AC02480722791E2100495FB9 /* IterableInboxNavigationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IterableInboxNavigationViewController.swift; sourceTree = "<group>"; };
@@ -1153,6 +1157,7 @@
11531157
5536781E2576FF9000DB3652 /* IterableUtilTests.swift */,
11541158
ACED4C00213F50B30055A497 /* LoggingTests.swift */,
11551159
55B37FC5229752DD0042F13A /* OrderedDictionaryTests.swift */,
1160+
94D8F9D52CDC294300D4CF53 /* ThreadSafeOrderedDictionaryTests.swift */,
11561161
ACEDF41E2183C436000B9BFE /* PendingTests.swift */,
11571162
);
11581163
name = "foundational-tests";
@@ -1280,6 +1285,7 @@
12801285
AC776DA5211A1B8A00C27C27 /* IterableRequestUtil.swift */,
12811286
AC72A0AD20CF4C16004D7997 /* IterableUtil.swift */,
12821287
AC32E16721DD55B900BD4F83 /* OrderedDictionary.swift */,
1288+
94D8F9D32CDC291000D4CF53 /* ThreadSafeOrderedDictionary.swift */,
12831289
ACD8BF852757FC4C00C2EAB2 /* UIColor+Extension.swift */,
12841290
);
12851291
name = Util;
@@ -2112,6 +2118,7 @@
21122118
ACD2B83D25B0A74A005D7A90 /* Models.swift in Sources */,
21132119
55DD2027269E5EA300773CC7 /* InboxViewControllerViewModelView.swift in Sources */,
21142120
AC1BED9523F1D4C700FDD75F /* MiscInboxClasses.swift in Sources */,
2121+
94D8F9D42CDC291000D4CF53 /* ThreadSafeOrderedDictionary.swift in Sources */,
21152122
553449A129C2621E002E4599 /* EmbeddedMessagingProcessor.swift in Sources */,
21162123
556FB1EA244FAF6A00EDF6BD /* InAppPresenter.swift in Sources */,
21172124
AC2AED4424EBC905000EE5F3 /* IterableTaskScheduler.swift in Sources */,
@@ -2222,6 +2229,7 @@
22222229
AC8F35A2239806B500302994 /* InboxViewControllerViewModelTests.swift in Sources */,
22232230
AC995F9A2166EEB50099A184 /* CommonMocks.swift in Sources */,
22242231
5588DFE128C046B7000697D7 /* MockLocalStorage.swift in Sources */,
2232+
94D8F9D62CDC294300D4CF53 /* ThreadSafeOrderedDictionaryTests.swift in Sources */,
22252233
1CBFFE1B2A97AEEF00ED57EE /* EmbeddedMessagingProcessorTests.swift in Sources */,
22262234
5588DF8128C04494000697D7 /* MockUrlDelegate.swift in Sources */,
22272235
5585DF8F22A73390000A32B9 /* IterableInboxViewControllerTests.swift in Sources */,

swift-sdk/Internal/InAppManager+Functions.swift

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,14 @@
55
import Foundation
66

77
enum MessagesProcessorResult {
8-
case show(message: IterableInAppMessage, messagesMap: OrderedDictionary<String, IterableInAppMessage>)
9-
case noShow(messagesMap: OrderedDictionary<String, IterableInAppMessage>)
8+
case show(message: IterableInAppMessage, messagesMap: ThreadSafeOrderedDictionary<String, IterableInAppMessage>)
9+
case noShow(messagesMap: ThreadSafeOrderedDictionary<String, IterableInAppMessage>)
1010
}
1111

1212
struct MessagesProcessor {
1313
init(inAppDelegate: IterableInAppDelegate,
1414
inAppDisplayChecker: InAppDisplayChecker,
15-
messagesMap: OrderedDictionary<String, IterableInAppMessage>) {
15+
messagesMap: ThreadSafeOrderedDictionary<String, IterableInAppMessage>) {
1616
ITBInfo()
1717

1818
self.inAppDelegate = inAppDelegate
@@ -97,18 +97,18 @@ struct MessagesProcessor {
9797

9898
private let inAppDelegate: IterableInAppDelegate
9999
private let inAppDisplayChecker: InAppDisplayChecker
100-
private var messagesMap: OrderedDictionary<String, IterableInAppMessage>
100+
private var messagesMap: ThreadSafeOrderedDictionary<String, IterableInAppMessage>
101101
}
102102

103103
struct MergeMessagesResult {
104104
let inboxChanged: Bool
105-
let messagesMap: OrderedDictionary<String, IterableInAppMessage>
105+
let messagesMap: ThreadSafeOrderedDictionary<String, IterableInAppMessage>
106106
let deliveredMessages: [IterableInAppMessage]
107107
}
108108

109109
/// Merges the results and determines whether inbox changed needs to be fired.
110110
struct MessagesObtainedHandler {
111-
init(messagesMap: OrderedDictionary<String, IterableInAppMessage>, messages: [IterableInAppMessage]) {
111+
init(messagesMap: ThreadSafeOrderedDictionary<String, IterableInAppMessage>, messages: [IterableInAppMessage]) {
112112
ITBInfo()
113113
self.messagesMap = messagesMap
114114
self.messages = messages
@@ -123,7 +123,7 @@ struct MessagesObtainedHandler {
123123
let addedInboxCount = addedMessages.reduce(0) { $1.saveToInbox ? $0 + 1 : $0 }
124124

125125
var messagesOverwritten = 0
126-
var newMessagesMap = OrderedDictionary<String, IterableInAppMessage>()
126+
let newMessagesMap = ThreadSafeOrderedDictionary<String, IterableInAppMessage>()
127127
messages.forEach { serverMessage in
128128
let messageId = serverMessage.messageId
129129
if let existingMessage = messagesMap[messageId] {
@@ -145,7 +145,7 @@ struct MessagesObtainedHandler {
145145
deliveredMessages: deliveredMessages)
146146
}
147147

148-
private let messagesMap: OrderedDictionary<String, IterableInAppMessage>
148+
private let messagesMap: ThreadSafeOrderedDictionary<String, IterableInAppMessage>
149149
private let messages: [IterableInAppMessage]
150150

151151
// We should only overwrite if the server is read and client is not read.

swift-sdk/Internal/InAppManager.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -284,7 +284,7 @@ class InAppManager: NSObject, IterableInternalInAppManagerProtocol {
284284
lastSyncTime = dateProvider.currentDate
285285
}
286286

287-
private func getMessagesMap(fromMessagesProcessorResult messagesProcessorResult: MessagesProcessorResult) -> OrderedDictionary<String, IterableInAppMessage> {
287+
private func getMessagesMap(fromMessagesProcessorResult messagesProcessorResult: MessagesProcessorResult) -> ThreadSafeOrderedDictionary<String, IterableInAppMessage> {
288288
switch messagesProcessorResult {
289289
case let .noShow(messagesMap: messagesMap):
290290
return messagesMap
@@ -303,7 +303,7 @@ class InAppManager: NSObject, IterableInternalInAppManagerProtocol {
303303
}
304304
}
305305

306-
private func processAndShowMessage(messagesMap: OrderedDictionary<String, IterableInAppMessage>) {
306+
private func processAndShowMessage(messagesMap: ThreadSafeOrderedDictionary<String, IterableInAppMessage>) {
307307
var processor = MessagesProcessor(inAppDelegate: inAppDelegate, inAppDisplayChecker: self, messagesMap: messagesMap)
308308
let messagesProcessorResult = processor.processMessages()
309309
self.messagesMap = getMessagesMap(fromMessagesProcessorResult: messagesProcessorResult)
@@ -552,7 +552,7 @@ class InAppManager: NSObject, IterableInternalInAppManagerProtocol {
552552
private let notificationCenter: NotificationCenterProtocol
553553

554554
private let persister: InAppPersistenceProtocol
555-
private var messagesMap = OrderedDictionary<String, IterableInAppMessage>()
555+
private var messagesMap = ThreadSafeOrderedDictionary<String, IterableInAppMessage>()
556556
private let dateProvider: DateProviderProtocol
557557
private var lastDismissedTime: Date?
558558
private var lastDisplayTime: Date?
@@ -607,7 +607,7 @@ extension InAppManager: InAppNotifiable {
607607
ITBInfo()
608608

609609
updateQueue.async { [weak self] in
610-
if let _ = self?.messagesMap.filter({ $0.key == messageId }).first {
610+
if let _ = self?.messagesMap.keys.filter({ $0 == messageId }).first {
611611
if let messagesMap = self?.messagesMap {
612612
self?.messagesMap.removeValue(forKey: messageId)
613613
self?.persister.persist(messagesMap.values)
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
//
2+
// ThreadSafeOrderedDictionary.swift
3+
// swift-sdk
4+
//
5+
// Created by Ricky on 11/6/24.
6+
// Copyright © 2024 Iterable. All rights reserved.
7+
//
8+
9+
import Foundation
10+
11+
public final class ThreadSafeOrderedDictionary<K: Hashable, V> {
12+
private var orderedDictionary = OrderedDictionary<K, V>()
13+
private let lock = NSRecursiveLock()
14+
15+
public var keys: [K] {
16+
lock.withLock {
17+
orderedDictionary.keys
18+
}
19+
}
20+
21+
public var count: Int {
22+
lock.withLock {
23+
orderedDictionary.count
24+
}
25+
}
26+
27+
public var values: [V] {
28+
lock.withLock {
29+
orderedDictionary.values
30+
}
31+
}
32+
33+
public subscript(key: K) -> V? {
34+
get {
35+
lock.withLock {
36+
orderedDictionary[key]
37+
}
38+
}
39+
set {
40+
lock.withLock {
41+
orderedDictionary[key] = newValue
42+
}
43+
}
44+
}
45+
46+
@discardableResult public func updateValue(_ value: V?, forKey key: K) -> V? {
47+
lock.withLock {
48+
orderedDictionary.updateValue(value, forKey: key)
49+
}
50+
}
51+
52+
@discardableResult public func removeValue(forKey key: K) -> V? {
53+
lock.withLock {
54+
orderedDictionary.removeValue(forKey: key)
55+
}
56+
}
57+
58+
public func reset() {
59+
lock.withLock {
60+
orderedDictionary.reset()
61+
}
62+
}
63+
64+
public func makeIterator() -> AnyIterator<(key: K, value: V)> {
65+
lock.withLock {
66+
orderedDictionary.makeIterator()
67+
}
68+
}
69+
70+
public var description: String {
71+
lock.withLock {
72+
orderedDictionary.description
73+
}
74+
}
75+
}
76+
77+
// Conformance to ExpressibleByDictionaryLiteral
78+
extension ThreadSafeOrderedDictionary: ExpressibleByDictionaryLiteral {
79+
public convenience init(dictionaryLiteral elements: (K, V)...) {
80+
self.init()
81+
for (key, value) in elements {
82+
self[key] = value
83+
}
84+
}
85+
}

tests/unit-tests/InAppMessageProcessorTests.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ class InAppMessageProcessorTests: XCTestCase {
1515
let serverMessage = Self.makeEmptyInboxMessage(messageId)
1616
serverMessage.read = true
1717

18-
let messagesMap: OrderedDictionary<String, IterableInAppMessage> = [messageId: localMessage]
18+
let messagesMap: ThreadSafeOrderedDictionary<String, IterableInAppMessage> = [messageId: localMessage]
1919
let newMessages = [serverMessage]
2020

2121
let result = MessagesObtainedHandler(messagesMap: messagesMap,
@@ -31,7 +31,7 @@ class InAppMessageProcessorTests: XCTestCase {
3131
let serverMessage2 = Self.makeEmptyInboxMessage("msg-3")
3232
serverMessage2.read = false
3333

34-
let messagesMap: OrderedDictionary<String, IterableInAppMessage> = ["msg-1": localMessage]
34+
let messagesMap: ThreadSafeOrderedDictionary<String, IterableInAppMessage> = ["msg-1": localMessage]
3535
let newMessages = [serverMessage1, serverMessage2]
3636

3737
let result = MessagesObtainedHandler(messagesMap: messagesMap,
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
//
2+
// ThreadSafeOrderedDictionaryTests.swift
3+
// unit-tests
4+
//
5+
// Created by Ricky on 11/6/24.
6+
// Copyright © 2024 Iterable. All rights reserved.
7+
//
8+
9+
import XCTest
10+
11+
@testable import IterableSDK
12+
13+
class ThreadSafeOrderedDictionaryTests: XCTestCase {
14+
func testConcurrentAccess() {
15+
let orderedDict = ThreadSafeOrderedDictionary<String, Int>()
16+
17+
// Initialize with some data
18+
orderedDict["initialKey"] = 0
19+
20+
let iterations = 1_000
21+
let concurrentQueue = DispatchQueue.global(qos: .userInitiated)
22+
23+
// Dispatch group to wait for all tasks to complete
24+
let dispatchGroup = DispatchGroup()
25+
26+
// Concurrently write to the dictionary
27+
dispatchGroup.enter()
28+
concurrentQueue.async {
29+
DispatchQueue.concurrentPerform(iterations: iterations) { i in
30+
print(i, "Here 1")
31+
orderedDict["key_\(i)"] = i
32+
}
33+
dispatchGroup.leave()
34+
}
35+
36+
// Concurrently read from the dictionary
37+
dispatchGroup.enter()
38+
concurrentQueue.async {
39+
DispatchQueue.concurrentPerform(iterations: iterations) { i in
40+
print(i, "Here 2")
41+
_ = orderedDict["key_\(i % 10)"]
42+
}
43+
dispatchGroup.leave()
44+
}
45+
46+
// Wait for all operations to finish
47+
let result = dispatchGroup.wait(timeout: .now() + 10)
48+
49+
// Assert that all operations completed
50+
XCTAssertEqual(result, .success, "Test timed out - possible deadlock or crash occurred.")
51+
52+
// Check if dictionary contains at least one expected entry
53+
XCTAssertGreaterThanOrEqual(orderedDict.count, 1, "Count should be greater than or equal to 1 after concurrent writes")
54+
}
55+
}

0 commit comments

Comments
 (0)