diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj
index ea176fdf4..086545ac3 100644
--- a/Session.xcodeproj/project.pbxproj
+++ b/Session.xcodeproj/project.pbxproj
@@ -809,6 +809,7 @@
 		FDBB25E32988B13800F1508E /* _004_AddJobPriority.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDBB25E22988B13800F1508E /* _004_AddJobPriority.swift */; };
 		FDBB25E72988BBBE00F1508E /* UIContextualAction+Theming.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDBB25E62988BBBD00F1508E /* UIContextualAction+Theming.swift */; };
 		FDC0F0042BFECE12002CBFB9 /* TimeUnit.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC0F0032BFECE12002CBFB9 /* TimeUnit.swift */; };
+		FDC0F0082C00721A002CBFB9 /* MockOGPoller.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC0F0072C00721A002CBFB9 /* MockOGPoller.swift */; };
 		FDC13D472A16E4CA007267C7 /* SubscribeRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC13D462A16E4CA007267C7 /* SubscribeRequest.swift */; };
 		FDC13D492A16EC20007267C7 /* Service.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC13D482A16EC20007267C7 /* Service.swift */; };
 		FDC13D4B2A16ECBA007267C7 /* SubscribeResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC13D4A2A16ECBA007267C7 /* SubscribeResponse.swift */; };
@@ -1996,6 +1997,7 @@
 		FDBB25E22988B13800F1508E /* _004_AddJobPriority.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _004_AddJobPriority.swift; sourceTree = "<group>"; };
 		FDBB25E62988BBBD00F1508E /* UIContextualAction+Theming.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIContextualAction+Theming.swift"; sourceTree = "<group>"; };
 		FDC0F0032BFECE12002CBFB9 /* TimeUnit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimeUnit.swift; sourceTree = "<group>"; };
+		FDC0F0072C00721A002CBFB9 /* MockOGPoller.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockOGPoller.swift; sourceTree = "<group>"; };
 		FDC13D462A16E4CA007267C7 /* SubscribeRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscribeRequest.swift; sourceTree = "<group>"; };
 		FDC13D482A16EC20007267C7 /* Service.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Service.swift; sourceTree = "<group>"; };
 		FDC13D4A2A16ECBA007267C7 /* SubscribeResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscribeResponse.swift; sourceTree = "<group>"; };
@@ -4434,6 +4436,7 @@
 			children = (
 				FDC438BC27BB2AB400C60D73 /* Mockable.swift */,
 				FD078E4C27E17156000769AF /* MockOGMCache.swift */,
+				FDC0F0072C00721A002CBFB9 /* MockOGPoller.swift */,
 			);
 			path = _TestUtilities;
 			sourceTree = "<group>";
@@ -6614,6 +6617,7 @@
 				FDC2909827D7129B005DAE71 /* PersonalizationSpec.swift in Sources */,
 				FD078E4D27E17156000769AF /* MockOGMCache.swift in Sources */,
 				FD9DD2722A72516D00ECB68E /* TestExtensions.swift in Sources */,
+				FDC0F0082C00721A002CBFB9 /* MockOGPoller.swift in Sources */,
 				FDC290A627D860CE005DAE71 /* Mock.swift in Sources */,
 				FDA1E83D29AC71A800C5C3BD /* LibSessionSpec.swift in Sources */,
 				FD83B9C027CF2294005E1583 /* TestConstants.swift in Sources */,
@@ -7981,7 +7985,7 @@
 				CLANG_WARN__ARC_BRIDGE_CAST_NONARC = YES;
 				CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
 				CODE_SIGN_IDENTITY = "iPhone Developer";
-				CURRENT_PROJECT_VERSION = 446;
+				CURRENT_PROJECT_VERSION = 447;
 				ENABLE_BITCODE = NO;
 				ENABLE_STRICT_OBJC_MSGSEND = YES;
 				ENABLE_TESTABILITY = YES;
@@ -8059,7 +8063,7 @@
 				CLANG_WARN__ARC_BRIDGE_CAST_NONARC = YES;
 				CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
 				CODE_SIGN_IDENTITY = "iPhone Distribution";
-				CURRENT_PROJECT_VERSION = 446;
+				CURRENT_PROJECT_VERSION = 447;
 				ENABLE_BITCODE = NO;
 				ENABLE_STRICT_OBJC_MSGSEND = YES;
 				GCC_NO_COMMON_BLOCKS = YES;
diff --git a/Session/Conversations/ConversationViewModel.swift b/Session/Conversations/ConversationViewModel.swift
index b91b9a744..c84169093 100644
--- a/Session/Conversations/ConversationViewModel.swift
+++ b/Session/Conversations/ConversationViewModel.swift
@@ -416,7 +416,7 @@ public class ConversationViewModel: OWSAudioPlayerDelegate {
                         initialUnreadInteractionId: self?.initialUnreadInteractionId
                     ),
                     currentDataRetriever: { self?.interactionData },
-                    onDataChange: self?.onInteractionChange,
+                    onDataChangeRetriever: { self?.onInteractionChange },
                     onUnobservedDataChange: { updatedData, changeset in
                         self?.unobservedInteractionDataChanges = (changeset.isEmpty ?
                             nil :
@@ -671,6 +671,11 @@ public class ConversationViewModel: OWSAudioPlayerDelegate {
     }
     
     private func forceUpdateDataIfPossible() {
+        // Ensure this is on the main thread as we access properties that could be accessed on other threads
+        guard Thread.isMainThread else {
+            return DispatchQueue.main.async { [weak self] in self?.forceUpdateDataIfPossible() }
+        }
+        
         // If we can't get the current page data then don't bother trying to update (it's not going to work)
         guard let currentPageInfo: PagedData.PageInfo = self.pagedDataObserver?.pageInfo.wrappedValue else { return }
         
@@ -685,7 +690,7 @@ public class ConversationViewModel: OWSAudioPlayerDelegate {
                 initialUnreadInteractionId: initialUnreadInteractionId
             ),
             currentDataRetriever: { [weak self] in self?.interactionData },
-            onDataChange: self.onInteractionChange,
+            onDataChangeRetriever: { [weak self] in self?.onInteractionChange },
             onUnobservedDataChange: { [weak self] updatedData, changeset in
                 self?.unobservedInteractionDataChanges = (changeset.isEmpty ?
                     nil :
diff --git a/Session/Home/HomeViewModel.swift b/Session/Home/HomeViewModel.swift
index 9ce940a93..3c20a8194 100644
--- a/Session/Home/HomeViewModel.swift
+++ b/Session/Home/HomeViewModel.swift
@@ -207,7 +207,7 @@ public class HomeViewModel {
                 PagedData.processAndTriggerUpdates(
                     updatedData: self?.process(data: updatedData, for: updatedPageInfo),
                     currentDataRetriever: { self?.threadData },
-                    onDataChange: self?.onThreadChange,
+                    onDataChangeRetriever: { self?.onThreadChange },
                     onUnobservedDataChange: { updatedData, changeset in
                         self?.unobservedThreadDataChanges = (changeset.isEmpty ?
                             nil :
@@ -286,7 +286,7 @@ public class HomeViewModel {
         PagedData.processAndTriggerUpdates(
             updatedData: updatedThreadData,
             currentDataRetriever: { [weak self] in (self?.unobservedThreadDataChanges?.0 ?? self?.threadData) },
-            onDataChange: onThreadChange,
+            onDataChangeRetriever: { [weak self] in self?.onThreadChange },
             onUnobservedDataChange: { [weak self] updatedData, changeset in
                 self?.unobservedThreadDataChanges = (changeset.isEmpty ?
                     nil :
diff --git a/Session/Media Viewing & Editing/MediaGalleryViewModel.swift b/Session/Media Viewing & Editing/MediaGalleryViewModel.swift
index 75a7fdabd..7f0c4a007 100644
--- a/Session/Media Viewing & Editing/MediaGalleryViewModel.swift	
+++ b/Session/Media Viewing & Editing/MediaGalleryViewModel.swift	
@@ -103,7 +103,7 @@ public class MediaGalleryViewModel {
                 PagedData.processAndTriggerUpdates(
                     updatedData: self?.process(data: updatedData, for: updatedPageInfo),
                     currentDataRetriever: { self?.galleryData },
-                    onDataChange: self?.onGalleryChange,
+                    onDataChangeRetriever: { self?.onGalleryChange },
                     onUnobservedDataChange: { updatedData, changeset in
                         self?.unobservedGalleryDataChanges = (changeset.isEmpty ?
                             nil :
diff --git a/Session/Meta/AppDelegate.swift b/Session/Meta/AppDelegate.swift
index dca17e1fb..411bfaf15 100644
--- a/Session/Meta/AppDelegate.swift
+++ b/Session/Meta/AppDelegate.swift
@@ -493,7 +493,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
         
         guard !Storage.isDatabasePasswordAccessible else { return }    // All good
         
-        Logger.info("Exiting because we are in the background and the database password is not accessible.")
+        Log.info("Exiting because we are in the background and the database password is not accessible.")
         
         let notificationContent: UNMutableNotificationContent = UNMutableNotificationContent()
         notificationContent.body = String(
diff --git a/Session/Meta/SessionApp.swift b/Session/Meta/SessionApp.swift
index eb464e26b..b0b75c576 100644
--- a/Session/Meta/SessionApp.swift
+++ b/Session/Meta/SessionApp.swift
@@ -114,6 +114,8 @@ public struct SessionApp {
     public static func resetAppData(onReset: (() -> ())? = nil) {
         Log.flush()
         LibSession.clearMemoryState()
+        LibSession.clearSnodeCache()
+        LibSession.closeNetworkConnections()
         Storage.resetAllStorage()
         ProfileManager.resetProfileStorage()
         Attachment.resetAttachmentStorage()
diff --git a/Session/Settings/BlockedContactsViewModel.swift b/Session/Settings/BlockedContactsViewModel.swift
index 35a934c71..9be1c3079 100644
--- a/Session/Settings/BlockedContactsViewModel.swift
+++ b/Session/Settings/BlockedContactsViewModel.swift
@@ -64,7 +64,7 @@ public class BlockedContactsViewModel: SessionTableViewModel, NavigatableStateHo
             onChangeUnsorted: { [weak self] updatedData, updatedPageInfo in
                 PagedData.processAndTriggerUpdates(
                     updatedData: self?.process(data: updatedData, for: updatedPageInfo)
-                        .mapToSessionTableViewData(for: self),
+                        .mapToSessionTableViewData(for: self),  // Update the cell positions for background rounding
                     currentDataRetriever: { self?.tableData },
                     valueSubject: self?.pendingTableDataSubject
                 )
diff --git a/Session/Settings/NukeDataModal.swift b/Session/Settings/NukeDataModal.swift
index 16ed7a9b8..48c9e70a6 100644
--- a/Session/Settings/NukeDataModal.swift
+++ b/Session/Settings/NukeDataModal.swift
@@ -282,9 +282,6 @@ final class NukeDataModal: Modal {
             $0.recentReactionTimestamps = []
         }
         
-        // Clear the Snode pool
-        LibSession.clearSnodeCache()
-        
         // Stop any pollers
         (UIApplication.shared.delegate as? AppDelegate)?.stopPollers()
         
diff --git a/SessionMessagingKit/Open Groups/OpenGroupManager.swift b/SessionMessagingKit/Open Groups/OpenGroupManager.swift
index 1816a95cd..5f50758d6 100644
--- a/SessionMessagingKit/Open Groups/OpenGroupManager.swift	
+++ b/SessionMessagingKit/Open Groups/OpenGroupManager.swift	
@@ -41,23 +41,21 @@ public final class OpenGroupManager {
             // Update the cache state and re-create all of the pollers
             dependencies.caches.mutate(cache: .openGroupManager) { cache in
                 cache.isPolling = true
-                cache.pollers = servers
-                    .reduce(into: [:]) { result, server in
-                        result[server.lowercased()]?.stop() // Should never occur
-                        result[server.lowercased()] = OpenGroupAPI.Poller(for: server.lowercased())
+                
+                servers
+                    .map { server -> OpenGroupAPI.PollerType in cache.getOrCreatePoller(for: server.lowercased()) }
+                    .forEach { poller in
+                        // Now that the pollers have been created actually start them
+                        poller.stop() // Should never be an issue
+                        poller.startIfNeeded(using: dependencies)
                     }
             }
-            
-            // Now that the pollers have been created actually start them
-            dependencies.caches[.openGroupManager].pollers
-                .forEach { _, poller in poller.startIfNeeded(using: dependencies) }
         }
     }
 
     public func stopPolling(using dependencies: Dependencies = Dependencies()) {
         dependencies.caches.mutate(cache: .openGroupManager) {
-            $0.pollers.forEach { _, openGroupPoller in openGroupPoller.stop() }
-            $0.pollers.removeAll()
+            $0.stopAndRemoveAllPollers()
             $0.isPolling = false
         }
     }
@@ -142,7 +140,7 @@ public final class OpenGroupManager {
         }
         
         // First check if there is no poller for the specified server
-        if Set(dependencies.caches[.openGroupManager].pollers.keys).intersection(serverOptions).isEmpty {
+        if Set(dependencies.caches[.openGroupManager].serversBeingPolled).intersection(serverOptions).isEmpty {
             return false
         }
         
@@ -295,16 +293,8 @@ public final class OpenGroupManager {
                             OpenGroupAPI.workQueue.async(using: dependencies) {
                                 // (Re)start the poller if needed (want to force it to poll immediately in the next
                                 // run loop to avoid a big delay before the next poll)
-                                let poller: OpenGroupAPI.Poller = dependencies.caches.mutate(cache: .openGroupManager) {
-                                    // Don't create a new poller instance if one already exists so we don't
-                                    // double up on pollers
-                                    guard let poller: OpenGroupAPI.Poller = $0.pollers[server.lowercased()] else {
-                                        let poller: OpenGroupAPI.Poller = OpenGroupAPI.Poller(for: server.lowercased())
-                                        $0.pollers[server.lowercased()] = poller
-                                        return poller
-                                    }
-                                    
-                                    return poller
+                                let poller: OpenGroupAPI.PollerType = dependencies.caches.mutate(cache: .openGroupManager) {
+                                    $0.getOrCreatePoller(for: server.lowercased())
                                 }
                                 
                                 poller.stop()
@@ -356,9 +346,9 @@ public final class OpenGroupManager {
             .defaulting(to: 1)
         
         if numActiveRooms == 1, let server: String = server?.lowercased() {
-            let poller = dependencies.caches[.openGroupManager].pollers[server]
-            poller?.stop()
-            dependencies.caches.mutate(cache: .openGroupManager) { $0.pollers[server] = nil }
+            dependencies.caches.mutate(cache: .openGroupManager) {
+                $0.stopAndRemovePoller(for: server)
+            }
         }
         
         // Remove all the data (everything should cascade delete)
@@ -1230,12 +1220,31 @@ public extension OpenGroupManager {
         public var defaultRoomsPublisher: AnyPublisher<[DefaultRoomInfo], Error>?
         public var groupImagePublishers: [String: AnyPublisher<Data, Error>] = [:]
         
-        public var pollers: [String: OpenGroupAPI.Poller] = [:] // One for each server
         public var isPolling: Bool = false
+        public var serversBeingPolled: Set<String> { return Set(_pollers.keys) }
         
         /// Server URL to value
         public var hasPerformedInitialPoll: [String: Bool] = [:]
         public var timeSinceLastPoll: [String: TimeInterval] = [:]
+        
+        private var _pollers: [String: OpenGroupAPI.PollerType] = [:] // One for each server
+        public func getOrCreatePoller(for server: String) -> OpenGroupAPI.PollerType {
+            guard let poller: OpenGroupAPI.PollerType = _pollers[server.lowercased()] else {
+                let poller: OpenGroupAPI.Poller = OpenGroupAPI.Poller(for: server.lowercased())
+                _pollers[server.lowercased()] = poller
+                return poller
+            }
+            
+            return poller
+        }
+        public func stopAndRemovePoller(for server: String) {
+            _pollers[server.lowercased()]?.stop()
+            _pollers[server.lowercased()] = nil
+        }
+        public func stopAndRemoveAllPollers() {
+            _pollers.forEach { _, poller in poller.stop() }
+            _pollers.removeAll()
+        }
 
         fileprivate var _timeSinceLastOpen: TimeInterval?
         public func getTimeSinceLastOpen(using dependencies: Dependencies) -> TimeInterval {
@@ -1272,8 +1281,8 @@ public protocol OGMImmutableCacheType: ImmutableCacheType {
     var defaultRoomsPublisher: AnyPublisher<[OpenGroupManager.DefaultRoomInfo], Error>? { get }
     var groupImagePublishers: [String: AnyPublisher<Data, Error>] { get }
     
-    var pollers: [String: OpenGroupAPI.Poller] { get }
     var isPolling: Bool { get }
+    var serversBeingPolled: Set<String> { get }
     
     var hasPerformedInitialPoll: [String: Bool] { get }
     var timeSinceLastPoll: [String: TimeInterval] { get }
@@ -1285,13 +1294,17 @@ public protocol OGMCacheType: OGMImmutableCacheType, MutableCacheType {
     var defaultRoomsPublisher: AnyPublisher<[OpenGroupManager.DefaultRoomInfo], Error>? { get set }
     var groupImagePublishers: [String: AnyPublisher<Data, Error>] { get set }
     
-    var pollers: [String: OpenGroupAPI.Poller] { get set }
     var isPolling: Bool { get set }
+    var serversBeingPolled: Set<String> { get }
     
     var hasPerformedInitialPoll: [String: Bool] { get set }
     var timeSinceLastPoll: [String: TimeInterval] { get set }
     
     var pendingChanges: [OpenGroupAPI.PendingChange] { get set }
     
+    func getOrCreatePoller(for server: String) -> OpenGroupAPI.PollerType
+    func stopAndRemovePoller(for server: String)
+    func stopAndRemoveAllPollers()
+    
     func getTimeSinceLastOpen(using dependencies: Dependencies) -> TimeInterval
 }
diff --git a/SessionMessagingKit/Sending & Receiving/Attachments/SignalAttachment.swift b/SessionMessagingKit/Sending & Receiving/Attachments/SignalAttachment.swift
index 45ebb30d1..dec044d86 100644
--- a/SessionMessagingKit/Sending & Receiving/Attachments/SignalAttachment.swift	
+++ b/SessionMessagingKit/Sending & Receiving/Attachments/SignalAttachment.swift	
@@ -1133,14 +1133,21 @@ public class SignalAttachment: Equatable, Hashable {
     // MARK: - Hashable
     
     public func hash(into hasher: inout Hasher) {
-        dataSource.hash(into: &hasher)
         dataUTI.hash(into: &hasher)
         captionText.hash(into: &hasher)
         linkPreviewDraft.hash(into: &hasher)
         isConvertibleToTextMessage.hash(into: &hasher)
         isConvertibleToContactShare.hash(into: &hasher)
-        cachedImage.hash(into: &hasher)
-        cachedVideoPreview.hash(into: &hasher)
         isVoiceMessage.hash(into: &hasher)
+        
+        /// There was a crash in `AttachmentApprovalViewController when trying to generate the hash
+        /// value to store in a dictionary, I'm guessing it's due to either `dataSource`, `cachedImage` or `cachedVideoPreview`
+        /// so, instead of trying to hash them directly which involves unknown behaviours due to `NSObject` & `UIImage` types, this
+        /// has been reworked to use primitives
+        dataSource.sourceFilename.hash(into: &hasher)
+        cachedImage?.size.width.hash(into: &hasher)
+        cachedImage?.size.height.hash(into: &hasher)
+        cachedVideoPreview?.size.width.hash(into: &hasher)
+        cachedVideoPreview?.size.height.hash(into: &hasher)
     }
 }
diff --git a/SessionMessagingKit/Sending & Receiving/Pollers/OpenGroupPoller.swift b/SessionMessagingKit/Sending & Receiving/Pollers/OpenGroupPoller.swift
index 7269e13e3..7bfc56073 100644
--- a/SessionMessagingKit/Sending & Receiving/Pollers/OpenGroupPoller.swift	
+++ b/SessionMessagingKit/Sending & Receiving/Pollers/OpenGroupPoller.swift	
@@ -7,7 +7,12 @@ import SessionSnodeKit
 import SessionUtilitiesKit
 
 extension OpenGroupAPI {
-    public final class Poller {
+    public protocol PollerType {
+        func startIfNeeded(using dependencies: Dependencies)
+        func stop()
+    }
+    
+    public final class Poller: PollerType {
         typealias PollResponse = (info: ResponseInfoType, data: [OpenGroupAPI.Endpoint: Decodable])
         
         private let server: String
diff --git a/SessionMessagingKitTests/LibSession/LibSessionUtilSpec.swift b/SessionMessagingKitTests/LibSession/LibSessionUtilSpec.swift
index 55cd0f556..6e192174e 100644
--- a/SessionMessagingKitTests/LibSession/LibSessionUtilSpec.swift
+++ b/SessionMessagingKitTests/LibSession/LibSessionUtilSpec.swift
@@ -305,7 +305,7 @@ fileprivate extension LibSessionUtilSpec {
                 
                 // Empty contacts shouldn't have an existing contact
                 let definitelyRealId: String = "050000000000000000000000000000000000000000000000000000000000000000"
-                var cDefinitelyRealId: [CChar] = definitelyRealId.cArray.nullTerminated()
+                var cDefinitelyRealId: [CChar] = definitelyRealId.cString(using: .utf8)!
                 let contactPtr: UnsafeMutablePointer<contacts_contact>? = nil
                 expect(contacts_get(conf, contactPtr, &cDefinitelyRealId)).to(beFalse())
                 
@@ -374,7 +374,7 @@ fileprivate extension LibSessionUtilSpec {
                 
                 // Pretend we uploaded it
                 let fakeHash1: String = "fakehash1"
-                var cFakeHash1: [CChar] = fakeHash1.cArray.nullTerminated()
+                var cFakeHash1: [CChar] = fakeHash1.cString(using: .utf8)!
                 config_confirm_pushed(conf, pushData2.pointee.seqno, &cFakeHash1)
                 expect(config_needs_push(conf)).to(beFalse())
                 expect(config_needs_dump(conf)).to(beTrue())
@@ -415,7 +415,7 @@ fileprivate extension LibSessionUtilSpec {
                 expect(contact4.created).to(equal(createdTs))
                 
                 let anotherId: String = "051111111111111111111111111111111111111111111111111111111111111111"
-                var cAnotherId: [CChar] = anotherId.cArray.nullTerminated()
+                var cAnotherId: [CChar] = anotherId.cString(using: .utf8)!
                 var contact5: contacts_contact = contacts_contact()
                 expect(contacts_get_or_construct(conf2, &contact5, &cAnotherId)).to(beTrue())
                 expect(String(libSessionVal: contact5.name)).to(beEmpty())
@@ -435,8 +435,8 @@ fileprivate extension LibSessionUtilSpec {
                 
                 // Check the merging
                 let fakeHash2: String = "fakehash2"
-                var cFakeHash2: [CChar] = fakeHash2.cArray.nullTerminated()
-                var mergeHashes: [UnsafePointer<CChar>?] = [cFakeHash2].unsafeCopy()
+                var cFakeHash2: [CChar] = fakeHash2.cString(using: .utf8)!
+                var mergeHashes: [UnsafePointer<CChar>?] = ((try? [cFakeHash2].unsafeCopyCStringArray()) ?? [])
                 var mergeData: [UnsafePointer<UInt8>?] = [UnsafePointer(pushData4.pointee.config)]
                 var mergeSize: [Int] = [pushData4.pointee.config_len]
                 let mergedHashes: UnsafeMutablePointer<config_string_list>? = config_merge(conf, &mergeHashes, &mergeData, &mergeSize, 1)
@@ -481,7 +481,7 @@ fileprivate extension LibSessionUtilSpec {
                 
                 // Client 2 adds a new friend:
                 let thirdId: String = "052222222222222222222222222222222222222222222222222222222222222222"
-                var cThirdId: [CChar] = thirdId.cArray.nullTerminated()
+                var cThirdId: [CChar] = thirdId.cString(using: .utf8)!
                 var contact7: contacts_contact = contacts_contact()
                 expect(contacts_get_or_construct(conf2, &contact7, &cThirdId)).to(beTrue())
                 contact7.nickname = "Nickname 3".toLibSession()
@@ -509,13 +509,13 @@ fileprivate extension LibSessionUtilSpec {
                     .to(equal([fakeHash2]))
                 
                 let fakeHash3a: String = "fakehash3a"
-                var cFakeHash3a: [CChar] = fakeHash3a.cArray.nullTerminated()
+                var cFakeHash3a: [CChar] = fakeHash3a.cString(using: .utf8)!
                 let fakeHash3b: String = "fakehash3b"
-                var cFakeHash3b: [CChar] = fakeHash3b.cArray.nullTerminated()
+                var cFakeHash3b: [CChar] = fakeHash3b.cString(using: .utf8)!
                 config_confirm_pushed(conf, pushData6.pointee.seqno, &cFakeHash3a)
                 config_confirm_pushed(conf2, pushData7.pointee.seqno, &cFakeHash3b)
                 
-                var mergeHashes2: [UnsafePointer<CChar>?] = [cFakeHash3b].unsafeCopy()
+                var mergeHashes2: [UnsafePointer<CChar>?] = ((try? [cFakeHash3b].unsafeCopyCStringArray()) ?? [])
                 var mergeData2: [UnsafePointer<UInt8>?] = [UnsafePointer(pushData7.pointee.config)]
                 var mergeSize2: [Int] = [pushData7.pointee.config_len]
                 let mergedHashes2: UnsafeMutablePointer<config_string_list>? = config_merge(conf, &mergeHashes2, &mergeData2, &mergeSize2, 1)
@@ -526,7 +526,7 @@ fileprivate extension LibSessionUtilSpec {
                 mergedHashes2?.deallocate()
                 pushData7.deallocate()
                 
-                var mergeHashes3: [UnsafePointer<CChar>?] = [cFakeHash3a].unsafeCopy()
+                var mergeHashes3: [UnsafePointer<CChar>?] = ((try? [cFakeHash3a].unsafeCopyCStringArray()) ?? [])
                 var mergeData3: [UnsafePointer<UInt8>?] = [UnsafePointer(pushData6.pointee.config)]
                 var mergeSize3: [Int] = [pushData6.pointee.config_len]
                 let mergedHashes3: UnsafeMutablePointer<config_string_list>? = config_merge(conf2, &mergeHashes3, &mergeData3, &mergeSize3, 1)
@@ -552,7 +552,7 @@ fileprivate extension LibSessionUtilSpec {
                     .to(equal([fakeHash3a, fakeHash3b]))
                 
                 let fakeHash4: String = "fakeHash4"
-                var cFakeHash4: [CChar] = fakeHash4.cArray.nullTerminated()
+                var cFakeHash4: [CChar] = fakeHash4.cString(using: .utf8)!
                 config_confirm_pushed(conf, pushData8.pointee.seqno, &cFakeHash4)
                 config_confirm_pushed(conf2, pushData9.pointee.seqno, &cFakeHash4)
                 pushData8.deallocate()
@@ -594,7 +594,7 @@ fileprivate extension LibSessionUtilSpec {
     ) throws -> contacts_contact {
         let postPrefixId: String = "05\(rand.nextBytes(count: 32).toHexString())"
         let sessionId: String = ("05\(index)a" + postPrefixId.suffix(postPrefixId.count - "05\(index)a".count))
-        var cSessionId: [CChar] = sessionId.cArray.nullTerminated()
+        var cSessionId: [CChar] = sessionId.cString(using: .utf8)!
         var contact: contacts_contact = contacts_contact()
         
         guard contacts_get_or_construct(conf, &contact, &cSessionId) else {
@@ -717,7 +717,7 @@ fileprivate extension LibSessionUtilSpec {
                 let pushData2: UnsafeMutablePointer<config_push_data> = config_push(conf)
                 expect(pushData2.pointee.seqno).to(equal(1))
                 
-                let expPush1Encrypted: [UInt8] = Data(hex: [
+                let expPush1Encrypted: [UInt8] = Array(Data(hex: [
                     "9693a69686da3055f1ecdfb239c3bf8e746951a36d888c2fb7c02e856a5c2091b24e39a7e1af828f",
                     "1fa09fe8bf7d274afde0a0847ba143c43ffb8722301b5ae32e2f078b9a5e19097403336e50b18c84",
                     "aade446cd2823b011f97d6ad2116a53feb814efecc086bc172d31f4214b4d7c630b63bbe575b0868",
@@ -725,7 +725,7 @@ fileprivate extension LibSessionUtilSpec {
                     "e7486ccde24416a7fd4a8ba5fa73899c65f4276dfaddd5b2100adcf0f793104fb235b31ce32ec656",
                     "056009a9ebf58d45d7d696b74e0c7ff0499c4d23204976f19561dc0dba6dc53a2497d28ce03498ea",
                     "49bf122762d7bc1d6d9c02f6d54f8384"
-                ].joined()).bytes
+                ].joined()))
                 
                 // We haven't dumped, so still need to dump:
                 expect(config_needs_dump(conf)).to(beTrue())
@@ -743,7 +743,7 @@ fileprivate extension LibSessionUtilSpec {
                 
                 // So now imagine we got back confirmation from the swarm that the push has been stored:
                 let fakeHash1: String = "fakehash1"
-                var cFakeHash1: [CChar] = fakeHash1.cArray.nullTerminated()
+                var cFakeHash1: [CChar] = fakeHash1.cString(using: .utf8)!
                 config_confirm_pushed(conf, pushData2.pointee.seqno, &cFakeHash1)
                 pushData2.deallocate()
                 
@@ -768,8 +768,8 @@ fileprivate extension LibSessionUtilSpec {
                 
                 // Now imagine we just pulled down the `exp_push1` string from the swarm; we merge it into
                 // conf2:
-                var mergeHashes: [UnsafePointer<CChar>?] = [cFakeHash1].unsafeCopy()
-                var mergeData: [UnsafePointer<UInt8>?] = [expPush1Encrypted].unsafeCopy()
+                var mergeHashes: [UnsafePointer<CChar>?] = ((try? [cFakeHash1].unsafeCopyCStringArray()) ?? [])
+                var mergeData: [UnsafePointer<UInt8>?] = ((try? [expPush1Encrypted].unsafeCopyUInt8Array()) ?? [])
                 var mergeSize: [Int] = [expPush1Encrypted.count]
                 let mergedHashes: UnsafeMutablePointer<config_string_list>? = config_merge(conf2, &mergeHashes, &mergeData, &mergeSize, 1)
                 expect([String](pointer: mergedHashes?.pointee.value, count: mergedHashes?.pointee.len))
@@ -820,13 +820,13 @@ fileprivate extension LibSessionUtilSpec {
                 expect(config_needs_push(conf2)).to(beTrue())
                 
                 let fakeHash2: String = "fakehash2"
-                var cFakeHash2: [CChar] = fakeHash2.cArray.nullTerminated()
+                var cFakeHash2: [CChar] = fakeHash2.cString(using: .utf8)!
                 let pushData3: UnsafeMutablePointer<config_push_data> = config_push(conf)
                 expect(pushData3.pointee.seqno).to(equal(2)) // incremented, since we made a field change
                 config_confirm_pushed(conf, pushData3.pointee.seqno, &cFakeHash2)
                 
                 let fakeHash3: String = "fakehash3"
-                var cFakeHash3: [CChar] = fakeHash3.cArray.nullTerminated()
+                var cFakeHash3: [CChar] = fakeHash3.cString(using: .utf8)!
                 let pushData4: UnsafeMutablePointer<config_push_data> = config_push(conf2)
                 expect(pushData4.pointee.seqno).to(equal(2)) // incremented, since we made a field change
                 config_confirm_pushed(conf, pushData4.pointee.seqno, &cFakeHash3)
@@ -852,7 +852,7 @@ fileprivate extension LibSessionUtilSpec {
                 
                 // Feed the new config into each other.  (This array could hold multiple configs if we pulled
                 // down more than one).
-                var mergeHashes2: [UnsafePointer<CChar>?] = [cFakeHash2].unsafeCopy()
+                var mergeHashes2: [UnsafePointer<CChar>?] = ((try? [cFakeHash2].unsafeCopyCStringArray()) ?? [])
                 var mergeData2: [UnsafePointer<UInt8>?] = [UnsafePointer(pushData3.pointee.config)]
                 var mergeSize2: [Int] = [pushData3.pointee.config_len]
                 let mergedHashes2: UnsafeMutablePointer<config_string_list>? = config_merge(conf2, &mergeHashes2, &mergeData2, &mergeSize2, 1)
@@ -862,7 +862,7 @@ fileprivate extension LibSessionUtilSpec {
                 mergedHashes2?.deallocate()
                 pushData3.deallocate()
                 
-                var mergeHashes3: [UnsafePointer<CChar>?] = [cFakeHash3].unsafeCopy()
+                var mergeHashes3: [UnsafePointer<CChar>?] = ((try? [cFakeHash3].unsafeCopyCStringArray()) ?? [])
                 var mergeData3: [UnsafePointer<UInt8>?] = [UnsafePointer(pushData4.pointee.config)]
                 var mergeSize3: [Int] = [pushData4.pointee.config_len]
                 let mergedHashes3: UnsafeMutablePointer<config_string_list>? = config_merge(conf, &mergeHashes3, &mergeData3, &mergeSize3, 1)
@@ -909,9 +909,9 @@ fileprivate extension LibSessionUtilSpec {
                 expect(user_profile_get_blinded_msgreqs(conf2)).to(equal(1))
                 
                 let fakeHash4: String = "fakehash4"
-                var cFakeHash4: [CChar] = fakeHash4.cArray.nullTerminated()
+                var cFakeHash4: [CChar] = fakeHash4.cString(using: .utf8)!
                 let fakeHash5: String = "fakehash5"
-                var cFakeHash5: [CChar] = fakeHash5.cArray.nullTerminated()
+                var cFakeHash5: [CChar] = fakeHash5.cString(using: .utf8)!
                 config_confirm_pushed(conf, pushData5.pointee.seqno, &cFakeHash4)
                 config_confirm_pushed(conf2, pushData6.pointee.seqno, &cFakeHash5)
                 pushData5.deallocate()
@@ -967,7 +967,7 @@ fileprivate extension LibSessionUtilSpec {
                 
                 // Empty contacts shouldn't have an existing contact
                 let definitelyRealId: String = "055000000000000000000000000000000000000000000000000000000000000000"
-                var cDefinitelyRealId: [CChar] = definitelyRealId.cArray.nullTerminated()
+                var cDefinitelyRealId: [CChar] = definitelyRealId.cString(using: .utf8)!
                 var oneToOne1: convo_info_volatile_1to1 = convo_info_volatile_1to1()
                 expect(convo_info_volatile_get_1to1(conf, &oneToOne1, &cDefinitelyRealId)).to(beFalse())
                 expect(convo_info_volatile_size(conf)).to(equal(0))
@@ -1001,7 +1001,7 @@ fileprivate extension LibSessionUtilSpec {
                 expect(config_needs_dump(conf)).to(beTrue())
                 
                 let openGroupBaseUrl: String = "http://Example.ORG:5678"
-                var cOpenGroupBaseUrl: [CChar] = openGroupBaseUrl.cArray.nullTerminated()
+                var cOpenGroupBaseUrl: [CChar] = openGroupBaseUrl.cString(using: .utf8)!
                 let openGroupBaseUrlResult: String = openGroupBaseUrl.lowercased()
                 //            ("http://Example.ORG:5678"
                 //                .lowercased()
@@ -1009,7 +1009,7 @@ fileprivate extension LibSessionUtilSpec {
                 //                [CChar](repeating: 0, count: (268 - openGroupBaseUrl.count))
                 //            )
                 let openGroupRoom: String = "SudokuRoom"
-                var cOpenGroupRoom: [CChar] = openGroupRoom.cArray.nullTerminated()
+                var cOpenGroupRoom: [CChar] = openGroupRoom.cString(using: .utf8)!
                 let openGroupRoomResult: String = openGroupRoom.lowercased()
                 //            ("SudokuRoom"
                 //                .lowercased()
@@ -1036,7 +1036,7 @@ fileprivate extension LibSessionUtilSpec {
                 
                 // Pretend we uploaded it
                 let fakeHash1: String = "fakehash1"
-                var cFakeHash1: [CChar] = fakeHash1.cArray.nullTerminated()
+                var cFakeHash1: [CChar] = fakeHash1.cString(using: .utf8)!
                 config_confirm_pushed(conf, pushData1.pointee.seqno, &cFakeHash1)
                 expect(config_needs_dump(conf)).to(beTrue())
                 expect(config_needs_push(conf)).to(beFalse())
@@ -1070,14 +1070,14 @@ fileprivate extension LibSessionUtilSpec {
                 community2.unread = true
                 
                 let anotherId: String = "051111111111111111111111111111111111111111111111111111111111111111"
-                var cAnotherId: [CChar] = anotherId.cArray.nullTerminated()
+                var cAnotherId: [CChar] = anotherId.cString(using: .utf8)!
                 var oneToOne5: convo_info_volatile_1to1 = convo_info_volatile_1to1()
                 expect(convo_info_volatile_get_or_construct_1to1(conf2, &oneToOne5, &cAnotherId)).to(beTrue())
                 oneToOne5.unread = true
                 convo_info_volatile_set_1to1(conf2, &oneToOne5)
                 
                 let thirdId: String = "05cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"
-                var cThirdId: [CChar] = thirdId.cArray.nullTerminated()
+                var cThirdId: [CChar] = thirdId.cString(using: .utf8)!
                 var legacyGroup2: convo_info_volatile_legacy_group = convo_info_volatile_legacy_group()
                 expect(convo_info_volatile_get_or_construct_legacy_group(conf2, &legacyGroup2, &cThirdId)).to(beTrue())
                 legacyGroup2.last_read = (nowTimestampMs - 50)
@@ -1089,8 +1089,8 @@ fileprivate extension LibSessionUtilSpec {
                 
                 // Check the merging
                 let fakeHash2: String = "fakehash2"
-                var cFakeHash2: [CChar] = fakeHash2.cArray.nullTerminated()
-                var mergeHashes: [UnsafePointer<CChar>?] = [cFakeHash2].unsafeCopy()
+                var cFakeHash2: [CChar] = fakeHash2.cString(using: .utf8)!
+                var mergeHashes: [UnsafePointer<CChar>?] = ((try? [cFakeHash2].unsafeCopyCStringArray()) ?? [])
                 var mergeData: [UnsafePointer<UInt8>?] = [UnsafePointer(pushData2.pointee.config)]
                 var mergeSize: [Int] = [pushData2.pointee.config_len]
                 let mergedHashes: UnsafeMutablePointer<config_string_list>? = config_merge(conf, &mergeHashes, &mergeData, &mergeSize, 1)
@@ -1141,7 +1141,7 @@ fileprivate extension LibSessionUtilSpec {
                 }
                 
                 let fourthId: String = "052000000000000000000000000000000000000000000000000000000000000000"
-                var cFourthId: [CChar] = fourthId.cArray.nullTerminated()
+                var cFourthId: [CChar] = fourthId.cString(using: .utf8)!
                 expect(config_needs_push(conf)).to(beFalse())
                 convo_info_volatile_erase_1to1(conf, &cFourthId)
                 expect(config_needs_push(conf)).to(beFalse())
@@ -1231,7 +1231,7 @@ fileprivate extension LibSessionUtilSpec {
                 
                 // Empty contacts shouldn't have an existing contact
                 let definitelyRealId: String = "055000000000000000000000000000000000000000000000000000000000000000"
-                var cDefinitelyRealId: [CChar] = definitelyRealId.cArray.nullTerminated()
+                var cDefinitelyRealId: [CChar] = definitelyRealId.cString(using: .utf8)!
                 let legacyGroup1: UnsafeMutablePointer<ugroups_legacy_group_info>? = user_groups_get_legacy_group(conf, &cDefinitelyRealId)
                 expect(legacyGroup1?.pointee).to(beNil())
                 expect(user_groups_size(conf)).to(equal(0))
@@ -1285,7 +1285,7 @@ fileprivate extension LibSessionUtilSpec {
                     "055555555555555555555555555555555555555555555555555555555555555555",
                     "056666666666666666666666666666666666666666666666666666666666666666"
                 ]
-                var cUsers: [[CChar]] = users.map { $0.cArray.nullTerminated() }
+                var cUsers: [[CChar]] = users.map { $0.cString(using: .utf8)! }
                 legacyGroup2.pointee.name = "Englishmen".toLibSession()
                 legacyGroup2.pointee.disappearing_timer = 60
                 legacyGroup2.pointee.joined_at = createdTs
@@ -1348,9 +1348,9 @@ fileprivate extension LibSessionUtilSpec {
                 ugroups_legacy_group_free(legacyGroup3)
                 
                 let communityPubkey: String = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
-                var cCommunityPubkey: [UInt8] = Data(hex: communityPubkey).cArray
-                var cCommunityBaseUrl: [CChar] = "http://Example.ORG:5678".cArray.nullTerminated()
-                var cCommunityRoom: [CChar] = "SudokuRoom".cArray.nullTerminated()
+                var cCommunityPubkey: [UInt8] = Array(Data(hex: communityPubkey))
+                var cCommunityBaseUrl: [CChar] = "http://Example.ORG:5678".cString(using: .utf8)!
+                var cCommunityRoom: [CChar] = "SudokuRoom".cString(using: .utf8)!
                 var community1: ugroups_community_info = ugroups_community_info()
                 expect(user_groups_get_or_construct_community(conf, &community1, &cCommunityBaseUrl, &cCommunityRoom, &cCommunityPubkey))
                     .to(beTrue())
@@ -1373,7 +1373,7 @@ fileprivate extension LibSessionUtilSpec {
                 
                 // Pretend we uploaded it
                 let fakeHash1: String = "fakehash1"
-                var cFakeHash1: [CChar] = fakeHash1.cArray.nullTerminated()
+                var cFakeHash1: [CChar] = fakeHash1.cString(using: .utf8)!
                 config_confirm_pushed(conf, pushData2.pointee.seqno, &cFakeHash1)
                 expect(config_needs_dump(conf)).to(beTrue())
                 expect(config_needs_push(conf)).to(beFalse())
@@ -1492,8 +1492,8 @@ fileprivate extension LibSessionUtilSpec {
                     ]))
                 }
                 
-                var cCommunity2BaseUrl: [CChar] = "http://example.org:5678".cArray.nullTerminated()
-                var cCommunity2Room: [CChar] = "sudokuRoom".cArray.nullTerminated()
+                var cCommunity2BaseUrl: [CChar] = "http://example.org:5678".cString(using: .utf8)!
+                var cCommunity2Room: [CChar] = "sudokuRoom".cString(using: .utf8)!
                 var community2: ugroups_community_info = ugroups_community_info()
                 expect(user_groups_get_community(conf2, &community2, &cCommunity2BaseUrl, &cCommunity2Room))
                     .to(beTrue())
@@ -1518,7 +1518,7 @@ fileprivate extension LibSessionUtilSpec {
                 expect(config_needs_dump(conf2)).to(beTrue())
                 
                 let fakeHash2: String = "fakehash2"
-                var cFakeHash2: [CChar] = fakeHash2.cArray.nullTerminated()
+                var cFakeHash2: [CChar] = fakeHash2.cString(using: .utf8)!
                 let pushData7: UnsafeMutablePointer<config_push_data> = config_push(conf2)
                 expect(pushData7.pointee.seqno).to(equal(2))
                 config_confirm_pushed(conf2, pushData7.pointee.seqno, &cFakeHash2)
@@ -1542,7 +1542,7 @@ fileprivate extension LibSessionUtilSpec {
                 config_confirm_pushed(conf2, pushData8.pointee.seqno, &cFakeHash2)
                 expect(config_needs_dump(conf2)).to(beFalse())
                 
-                var mergeHashes1: [UnsafePointer<CChar>?] = [cFakeHash2].unsafeCopy()
+                var mergeHashes1: [UnsafePointer<CChar>?] = ((try? [cFakeHash2].unsafeCopyCStringArray()) ?? [])
                 var mergeData1: [UnsafePointer<UInt8>?] = [UnsafePointer(pushData8.pointee.config)]
                 var mergeSize1: [Int] = [pushData8.pointee.config_len]
                 let mergedHashes1: UnsafeMutablePointer<config_string_list>? = config_merge(conf, &mergeHashes1, &mergeData1, &mergeSize1, 1)
@@ -1552,8 +1552,8 @@ fileprivate extension LibSessionUtilSpec {
                 mergedHashes1?.deallocate()
                 pushData8.deallocate()
                 
-                var cCommunity3BaseUrl: [CChar] = "http://example.org:5678".cArray.nullTerminated()
-                var cCommunity3Room: [CChar] = "SudokuRoom".cArray.nullTerminated()
+                var cCommunity3BaseUrl: [CChar] = "http://example.org:5678".cString(using: .utf8)!
+                var cCommunity3Room: [CChar] = "SudokuRoom".cString(using: .utf8)!
                 var community3: ugroups_community_info = ugroups_community_info()
                 expect(user_groups_get_community(conf, &community3, &cCommunity3BaseUrl, &cCommunity3Room))
                     .to(beTrue())
@@ -1581,12 +1581,12 @@ fileprivate extension LibSessionUtilSpec {
                 expect(config_needs_push(conf2)).to(beTrue())
                 expect(config_needs_dump(conf2)).to(beTrue())
                 
-                var cCommunity4BaseUrl: [CChar] = "http://exAMple.ORG:5678".cArray.nullTerminated()
-                var cCommunity4Room: [CChar] = "sudokuROOM".cArray.nullTerminated()
+                var cCommunity4BaseUrl: [CChar] = "http://exAMple.ORG:5678".cString(using: .utf8)!
+                var cCommunity4Room: [CChar] = "sudokuROOM".cString(using: .utf8)!
                 user_groups_erase_community(conf2, &cCommunity4BaseUrl, &cCommunity4Room)
                 
                 let fakeHash3: String = "fakehash3"
-                var cFakeHash3: [CChar] = fakeHash3.cArray.nullTerminated()
+                var cFakeHash3: [CChar] = fakeHash3.cString(using: .utf8)!
                 let pushData10: UnsafeMutablePointer<config_push_data> = config_push(conf2)
                 config_confirm_pushed(conf2, pushData10.pointee.seqno, &cFakeHash3)
                 
@@ -1599,7 +1599,7 @@ fileprivate extension LibSessionUtilSpec {
                     .to(equal([fakeHash3]))
                 currentHashes4?.deallocate()
                 
-                var mergeHashes2: [UnsafePointer<CChar>?] = [cFakeHash3].unsafeCopy()
+                var mergeHashes2: [UnsafePointer<CChar>?] = ((try? [cFakeHash3].unsafeCopyCStringArray()) ?? [])
                 var mergeData2: [UnsafePointer<UInt8>?] = [UnsafePointer(pushData10.pointee.config)]
                 var mergeSize2: [Int] = [pushData10.pointee.config_len]
                 let mergedHashes2: UnsafeMutablePointer<config_string_list>? = config_merge(conf, &mergeHashes2, &mergeData2, &mergeSize2, 1)
@@ -1613,13 +1613,13 @@ fileprivate extension LibSessionUtilSpec {
                 expect(user_groups_size_legacy_groups(conf)).to(equal(1))
                 
                 var prio: Int32 = 0
-                var cBeanstalkBaseUrl: [CChar] = "http://jacksbeanstalk.org".cArray.nullTerminated()
-                var cBeanstalkPubkey: [UInt8] = Data(
+                var cBeanstalkBaseUrl: [CChar] = "http://jacksbeanstalk.org".cString(using: .utf8)!
+                var cBeanstalkPubkey: [UInt8] = Array(Data(
                     hex: "0000111122223333444455556666777788889999aaaabbbbccccddddeeeeffff"
-                ).cArray
+                ))
                 
                 ["fee", "fi", "fo", "fum"].forEach { room in
-                    var cRoom: [CChar] = room.cArray.nullTerminated()
+                    var cRoom: [CChar] = room.cString(using: .utf8)!
                     prio += 1
                     
                     var community4: ugroups_community_info = ugroups_community_info()
@@ -1634,7 +1634,7 @@ fileprivate extension LibSessionUtilSpec {
                 expect(user_groups_size_legacy_groups(conf)).to(equal(1))
                 
                 let fakeHash4: String = "fakehash4"
-                var cFakeHash4: [CChar] = fakeHash4.cArray.nullTerminated()
+                var cFakeHash4: [CChar] = fakeHash4.cString(using: .utf8)!
                 let pushData11: UnsafeMutablePointer<config_push_data> = config_push(conf)
                 config_confirm_pushed(conf, pushData11.pointee.seqno, &cFakeHash4)
                 expect(pushData11.pointee.seqno).to(equal(4))
@@ -1643,12 +1643,12 @@ fileprivate extension LibSessionUtilSpec {
                 
                 // Load some obsolete ones in just to check that they get immediately obsoleted
                 let fakeHash10: String = "fakehash10"
-                let cFakeHash10: [CChar] = fakeHash10.cArray.nullTerminated()
+                let cFakeHash10: [CChar] = fakeHash10.cString(using: .utf8)!
                 let fakeHash11: String = "fakehash11"
-                let cFakeHash11: [CChar] = fakeHash11.cArray.nullTerminated()
+                let cFakeHash11: [CChar] = fakeHash11.cString(using: .utf8)!
                 let fakeHash12: String = "fakehash12"
-                let cFakeHash12: [CChar] = fakeHash12.cArray.nullTerminated()
-                var mergeHashes3: [UnsafePointer<CChar>?] = [cFakeHash10, cFakeHash11, cFakeHash12, cFakeHash4].unsafeCopy()
+                let cFakeHash12: [CChar] = fakeHash12.cString(using: .utf8)!
+                var mergeHashes3: [UnsafePointer<CChar>?] = ((try? [cFakeHash10, cFakeHash11, cFakeHash12, cFakeHash4].unsafeCopyCStringArray()) ?? [])
                 var mergeData3: [UnsafePointer<UInt8>?] = [
                     UnsafePointer(pushData10.pointee.config),
                     UnsafePointer(pushData2.pointee.config),
diff --git a/SessionMessagingKitTests/LibSession/Utilities/LibSessionTypeConversionUtilitiesSpec.swift b/SessionMessagingKitTests/LibSession/Utilities/LibSessionTypeConversionUtilitiesSpec.swift
index 313372b5f..c1624604e 100644
--- a/SessionMessagingKitTests/LibSession/Utilities/LibSessionTypeConversionUtilitiesSpec.swift
+++ b/SessionMessagingKitTests/LibSession/Utilities/LibSessionTypeConversionUtilitiesSpec.swift
@@ -12,11 +12,6 @@ class LibSessionTypeConversionUtilitiesSpec: QuickSpec {
     override class func spec() {
         // MARK: - a String
         describe("a String") {
-            // MARK: -- can convert to a cArray
-            it("can convert to a cArray") {
-                expect("Test123".cArray).to(equal([84, 101, 115, 116, 49, 50, 51]))
-            }
-            
             // MARK: -- can contain emoji
             it("can contain emoji") {
                 let original: String = "Hi 👋"
@@ -187,11 +182,6 @@ class LibSessionTypeConversionUtilitiesSpec: QuickSpec {
         
         // MARK: - Data
         describe("Data") {
-            // MARK: -- can convert to a cArray
-            it("can convert to a cArray") {
-                expect(Data([1, 2, 3]).cArray).to(equal([1, 2, 3]))
-            }
-            
             // MARK: -- when initialised with a libSession value
             context("when initialised with a libSession value") {
                 // MARK: ---- returns truncated data when given the wrong length
@@ -297,9 +287,9 @@ class LibSessionTypeConversionUtilitiesSpec: QuickSpec {
                 // MARK: ---- returns the correct array
                 it("returns the correct array") {
                     var test: [CChar] = (
-                        "Test1".cArray.nullTerminated() +
-                        "Test2".cArray.nullTerminated() +
-                        "Test3AndExtra".cArray.nullTerminated()
+                        "Test1".cString(using: .utf8)! +
+                        "Test2".cString(using: .utf8)! +
+                        "Test3AndExtra".cString(using: .utf8)!
                     )
                     let result = test.withUnsafeMutableBufferPointer { ptr in
                         var mutablePtr = UnsafeMutablePointer(ptr.baseAddress)
@@ -325,9 +315,9 @@ class LibSessionTypeConversionUtilitiesSpec: QuickSpec {
                 // MARK: ---- handles empty strings without issues
                 it("handles empty strings without issues") {
                     var test: [CChar] = (
-                        "Test1".cArray.nullTerminated() +
-                        "".cArray.nullTerminated() +
-                        "Test2".cArray.nullTerminated()
+                        "Test1".cString(using: .utf8)! +
+                        "".cString(using: .utf8)! +
+                        "Test2".cString(using: .utf8)!
                     )
                     let result = test.withUnsafeMutableBufferPointer { ptr in
                         var mutablePtr = UnsafeMutablePointer(ptr.baseAddress)
@@ -345,7 +335,7 @@ class LibSessionTypeConversionUtilitiesSpec: QuickSpec {
                 
                 // MARK: ---- returns null when given a null count
                 it("returns null when given a null count") {
-                    var test: [CChar] = "Test1".cArray.nullTerminated()
+                    var test: [CChar] = "Test1".cString(using: .utf8)!
                     let result = test.withUnsafeMutableBufferPointer { ptr in
                         var mutablePtr = UnsafeMutablePointer(ptr.baseAddress)
                         
diff --git a/SessionMessagingKitTests/Open Groups/OpenGroupManagerSpec.swift b/SessionMessagingKitTests/Open Groups/OpenGroupManagerSpec.swift
index af505b931..65948b162 100644
--- a/SessionMessagingKitTests/Open Groups/OpenGroupManagerSpec.swift	
+++ b/SessionMessagingKitTests/Open Groups/OpenGroupManagerSpec.swift	
@@ -174,11 +174,20 @@ class OpenGroupManagerSpec: QuickSpec {
                 cache.when { $0.encodedPublicKey }.thenReturn("05\(TestConstants.publicKey)")
             }
         )
+        @TestState var mockPoller: MockOGPoller! = MockOGPoller(
+            initialSetup: { poller in
+                poller.when { $0.stop() }.thenReturn(())
+                poller.when { $0.startIfNeeded(using: any()) }.thenReturn(())
+            }
+        )
         @TestState var mockOGMCache: MockOGMCache! = MockOGMCache(
             initialSetup: { cache in
                 cache.when { $0.pendingChanges }.thenReturn([])
-                cache.when { $0.pollers = any() }.thenReturn(())
                 cache.when { $0.isPolling = any() }.thenReturn(())
+                cache.when { $0.serversBeingPolled }.thenReturn([])
+                cache.when { $0.hasPerformedInitialPoll }.thenReturn([:])
+                cache.when { $0.timeSinceLastPoll }.thenReturn([:])
+                cache.when { $0.getTimeSinceLastOpen(using: any()) }.thenReturn(0)
                 cache
                     .when { $0.defaultRoomsPublisher = any(type: [OpenGroupManager.DefaultRoomInfo].self) }
                     .thenReturn(())
@@ -188,6 +197,9 @@ class OpenGroupManagerSpec: QuickSpec {
                 cache
                     .when { $0.pendingChanges = any(type: OpenGroupAPI.PendingChange.self) }
                     .thenReturn(())
+                cache.when { $0.getOrCreatePoller(for: any()) }.thenReturn(mockPoller)
+                cache.when { $0.stopAndRemovePoller(for: any()) }.thenReturn(())
+                cache.when { $0.stopAndRemoveAllPollers() }.thenReturn(())
             }
         )
         @TestState var mockCaches: MockCaches! = MockCaches()
@@ -209,13 +221,6 @@ class OpenGroupManagerSpec: QuickSpec {
         
         // MARK: - an OpenGroupManager
         describe("an OpenGroupManager") {
-            
-            afterEach {
-                // Just in case the shared instance had pollers created we should stop them
-                OpenGroupManager.shared.stopPolling()
-                openGroupManager.stopPolling()
-            }
-            
             // MARK: -- cache data
             context("cache data") {
                 // MARK: ---- defaults the time since last open to greatestFiniteMagnitude
@@ -293,7 +298,6 @@ class OpenGroupManagerSpec: QuickSpec {
                         }
                         .thenReturn(0)
                     mockOGMCache.when { $0.isPolling }.thenReturn(false)
-                    mockOGMCache.when { $0.pollers }.thenReturn([:])
                     
                     mockUserDefaults
                         .when { (defaults: inout any UserDefaultsType) -> Any? in
@@ -308,10 +312,11 @@ class OpenGroupManagerSpec: QuickSpec {
                     
                     expect(mockOGMCache)
                         .to(call(matchingParameters: true) {
-                            $0.pollers = [
-                                "testserver": OpenGroupAPI.Poller(for: "testserver"),
-                                "testserver1": OpenGroupAPI.Poller(for: "testserver1")
-                            ]
+                            $0.getOrCreatePoller(for: "testserver")
+                        })
+                    expect(mockOGMCache)
+                        .to(call(matchingParameters: true) {
+                            $0.getOrCreatePoller(for: "testserver1")
                         })
                 }
                 
@@ -329,7 +334,7 @@ class OpenGroupManagerSpec: QuickSpec {
                     
                     openGroupManager.startPolling(using: dependencies)
                     
-                    expect(mockOGMCache).toNot(call { $0.pollers })
+                    expect(mockOGMCache).toNot(call { $0.getOrCreatePoller(for: any()) })
                 }
             }
             
@@ -352,14 +357,13 @@ class OpenGroupManagerSpec: QuickSpec {
                     }
                     
                     mockOGMCache.when { $0.isPolling }.thenReturn(true)
-                    mockOGMCache.when { $0.pollers }.thenReturn(["testserver": OpenGroupAPI.Poller(for: "testserver")])
                 }
                 
                 // MARK: ---- removes all pollers
                 it("removes all pollers") {
                     openGroupManager.stopPolling(using: dependencies)
                     
-                    expect(mockOGMCache).to(call(matchingParameters: true) { $0.pollers = [:] })
+                    expect(mockOGMCache).to(call(matchingParameters: true) { $0.stopAndRemoveAllPollers() })
                 }
                 
                 // MARK: ---- updates the isPolling flag
@@ -443,13 +447,12 @@ class OpenGroupManagerSpec: QuickSpec {
             context("when checking it has an existing open group") {
                 // MARK: ---- when there is a thread for the room and the cache has a poller
                 context("when there is a thread for the room and the cache has a poller") {
+                    beforeEach {
+                        mockOGMCache.when { $0.serversBeingPolled }.thenReturn(["testserver"])
+                    }
+                    
                     // MARK: ------ for the no-scheme variant
                     context("for the no-scheme variant") {
-                        beforeEach {
-                            mockOGMCache.when { $0.pollers }
-                                .thenReturn(["testserver": OpenGroupAPI.Poller(for: "testserver")])
-                        }
-                        
                         // MARK: -------- returns true when no scheme is provided
                         it("returns true when no scheme is provided") {
                             expect(
@@ -501,11 +504,6 @@ class OpenGroupManagerSpec: QuickSpec {
                     
                     // MARK: ------ for the http variant
                     context("for the http variant") {
-                        beforeEach {
-                            mockOGMCache.when { $0.pollers }
-                                .thenReturn(["http://testserver": OpenGroupAPI.Poller(for: "http://testserver")])
-                        }
-                        
                         // MARK: -------- returns true when no scheme is provided
                         it("returns true when no scheme is provided") {
                             expect(
@@ -557,11 +555,6 @@ class OpenGroupManagerSpec: QuickSpec {
                     
                     // MARK: ------ for the https variant
                     context("for the https variant") {
-                        beforeEach {
-                            mockOGMCache.when { $0.pollers }
-                                .thenReturn(["https://testserver": OpenGroupAPI.Poller(for: "https://testserver")])
-                        }
-                        
                         // MARK: -------- returns true when no scheme is provided
                         it("returns true when no scheme is provided") {
                             expect(
@@ -616,8 +609,7 @@ class OpenGroupManagerSpec: QuickSpec {
                 context("when given the legacy DNS host and there is a cached poller for the default server") {
                     // MARK: ------ returns true
                     it("returns true") {
-                        mockOGMCache.when { $0.pollers }
-                            .thenReturn(["http://116.203.70.33": OpenGroupAPI.Poller(for: "http://116.203.70.33")])
+                        mockOGMCache.when { $0.serversBeingPolled }.thenReturn(["http://116.203.70.33"])
                         mockStorage.write { db in
                             try SessionThread(
                                 id: OpenGroup.idFor(roomToken: "testRoom", server: "http://116.203.70.33"),
@@ -651,7 +643,7 @@ class OpenGroupManagerSpec: QuickSpec {
                 context("when given the default server and there is a cached poller for the legacy DNS host") {
                     // MARK: ------ returns true
                     it("returns true") {
-                        mockOGMCache.when { $0.pollers }.thenReturn(["http://open.getsession.org": OpenGroupAPI.Poller(for: "http://open.getsession.org")])
+                        mockOGMCache.when { $0.serversBeingPolled }.thenReturn(["http://open.getsession.org"])
                         mockStorage.write { db in
                             try SessionThread(
                                 id: OpenGroup.idFor(roomToken: "testRoom", server: "http://open.getsession.org"),
@@ -683,8 +675,6 @@ class OpenGroupManagerSpec: QuickSpec {
                 
                 // MARK: ---- returns false when given an invalid server
                 it("returns false when given an invalid server") {
-                    mockOGMCache.when { $0.pollers }.thenReturn(["testserver": OpenGroupAPI.Poller(for: "testserver")])
-                    
                     expect(
                         mockStorage.read { db -> Bool in
                             openGroupManager
@@ -701,7 +691,7 @@ class OpenGroupManagerSpec: QuickSpec {
                 
                 // MARK: ---- returns false if there is not a poller for the server in the cache
                 it("returns false if there is not a poller for the server in the cache") {
-                    mockOGMCache.when { $0.pollers }.thenReturn([:])
+                    mockOGMCache.when { $0.serversBeingPolled }.thenReturn([])
                     
                     expect(
                         mockStorage.read { db -> Bool in
@@ -719,7 +709,6 @@ class OpenGroupManagerSpec: QuickSpec {
                 
                 // MARK: ---- returns false if there is a poller for the server in the cache but no thread for the room
                 it("returns false if there is a poller for the server in the cache but no thread for the room") {
-                    mockOGMCache.when { $0.pollers }.thenReturn(["testserver": OpenGroupAPI.Poller(for: "testserver")])
                     mockStorage.write { db in
                         try SessionThread.deleteAll(db)
                     }
@@ -749,7 +738,6 @@ class OpenGroupManagerSpec: QuickSpec {
                     mockNetwork
                         .when { $0.send(.onionRequest(any(), to: any(), with: any()), using: dependencies) }
                         .thenReturn(Network.BatchResponse.mockCapabilitiesAndRoomResponse)
-                    mockOGMCache.when { $0.pollers }.thenReturn([:])
                     
                     mockUserDefaults
                         .when { (defaults: inout any UserDefaultsType) -> Any? in
@@ -824,15 +812,18 @@ class OpenGroupManagerSpec: QuickSpec {
                     
                     expect(mockOGMCache)
                         .to(call(matchingParameters: true) {
-                            $0.pollers = ["testserver": OpenGroupAPI.Poller(for: "testserver")]
+                            $0.getOrCreatePoller(for: "testserver")
+                        })
+                    expect(mockPoller)
+                        .to(call(matchingParameters: true) { [dependencies = dependencies!] in
+                            $0.startIfNeeded(using: dependencies)
                         })
                 }
                 
                 // MARK: ---- an existing room
                 context("an existing room") {
                     beforeEach {
-                        mockOGMCache.when { $0.pollers }
-                            .thenReturn(["testserver": OpenGroupAPI.Poller(for: "testserver")])
+                        mockOGMCache.when { $0.serversBeingPolled }.thenReturn(["testserver"])
                         mockStorage.write { db in
                             try testOpenGroup.insert(db)
                         }
@@ -954,8 +945,6 @@ class OpenGroupManagerSpec: QuickSpec {
                                     .set(to: OpenGroup.idFor(roomToken: "testRoom", server: "testServer"))
                             )
                     }
-                    
-                    mockOGMCache.when { $0.pollers }.thenReturn([:])
                 }
                 
                 // MARK: ---- removes all interactions for the thread
@@ -994,8 +983,6 @@ class OpenGroupManagerSpec: QuickSpec {
                 context("and there is only one open group for this server") {
                     // MARK: ------ stops the poller
                     it("stops the poller") {
-                        mockOGMCache.when { $0.pollers }.thenReturn(["testserver": OpenGroupAPI.Poller(for: "testserver")])
-                        
                         mockStorage.write { db in
                             openGroupManager
                                 .delete(
@@ -1006,7 +993,7 @@ class OpenGroupManagerSpec: QuickSpec {
                                 )
                         }
                         
-                        expect(mockOGMCache).to(call(matchingParameters: true) { $0.pollers = [:] })
+                        expect(mockOGMCache).to(call(matchingParameters: true) { $0.stopAndRemovePoller(for: "testserver") })
                     }
                     
                     // MARK: ------ removes the open group
@@ -1175,7 +1162,6 @@ class OpenGroupManagerSpec: QuickSpec {
                         try testOpenGroup.insert(db)
                     }
                     
-                    mockOGMCache.when { $0.pollers }.thenReturn([:])
                     mockOGMCache.when { $0.hasPerformedInitialPoll }.thenReturn([:])
                     mockOGMCache.when { $0.timeSinceLastPoll }.thenReturn([:])
                     mockOGMCache
@@ -1575,54 +1561,6 @@ class OpenGroupManagerSpec: QuickSpec {
                     }
                 }
                 
-                // MARK: ---- when checking to start polling
-                context("when checking to start polling") {
-                    // MARK: ------ starts a new poller when not already polling
-                    it("starts a new poller when not already polling") {
-                        mockOGMCache.when { $0.pollers }.thenReturn([:])
-                        
-                        mockStorage.write { db in
-                            try OpenGroupManager.handlePollInfo(
-                                db,
-                                pollInfo: testPollInfo,
-                                publicKey: TestConstants.publicKey,
-                                for: "testRoom",
-                                on: "testServer",
-                                using: dependencies
-                            )
-                        }
-                        
-                        expect(mockOGMCache)
-                            .to(call(matchingParameters: true) {
-                                $0.pollers = ["testserver": OpenGroupAPI.Poller(for: "testserver")]
-                            })
-                    }
-                    
-                    // MARK: ------ restarts the poller if there already is one
-                    it("restarts the poller if there already is one") {
-                        mockOGMCache.when { $0.pollers }.thenReturn(["testserver": OpenGroupAPI.Poller(for: "testserver")])
-                        mockNetwork
-                            .when { $0.send(.onionRequest(any(), to: any(), with: any()), using: dependencies) }
-                            .thenReturn(Network.BatchResponse.mockBlindedPollResponse)
-                        
-                        mockStorage.write { db in
-                            try OpenGroupManager.handlePollInfo(
-                                db,
-                                pollInfo: testPollInfo,
-                                publicKey: TestConstants.publicKey,
-                                for: "testRoom",
-                                on: "testServer",
-                                using: dependencies
-                            )
-                        }
-                        
-                        expect(mockOGMCache)
-                            .to(call(matchingParameters: true) {
-                                $0.pollers = ["testserver": OpenGroupAPI.Poller(for: "testserver")]
-                            })
-                    }
-                }
-                
                 // MARK: ---- when trying to get the room image
                 context("when trying to get the room image") {
                     beforeEach {
diff --git a/SessionMessagingKitTests/_TestUtilities/MockOGMCache.swift b/SessionMessagingKitTests/_TestUtilities/MockOGMCache.swift
index d326d0a15..e22ff41a3 100644
--- a/SessionMessagingKitTests/_TestUtilities/MockOGMCache.swift
+++ b/SessionMessagingKitTests/_TestUtilities/MockOGMCache.swift
@@ -17,16 +17,15 @@ class MockOGMCache: Mock<OGMCacheType>, OGMCacheType {
         set { accept(args: [newValue]) }
     }
     
-    var pollers: [String: OpenGroupAPI.Poller] {
-        get { return accept() as! [String: OpenGroupAPI.Poller] }
-        set { accept(args: [newValue]) }
-    }
-    
     var isPolling: Bool {
         get { return accept() as! Bool }
         set { accept(args: [newValue]) }
     }
     
+    var serversBeingPolled: Set<String> {
+        get { return accept() as! Set<String> }
+    }
+    
     var hasPerformedInitialPoll: [String: Bool] {
         get { return accept() as! [String: Bool] }
         set { accept(args: [newValue]) }
@@ -42,6 +41,12 @@ class MockOGMCache: Mock<OGMCacheType>, OGMCacheType {
         set { accept(args: [newValue]) }
     }
     
+    func getOrCreatePoller(for server: String) -> OpenGroupAPI.PollerType {
+        return accept(args: [server]) as! OpenGroupAPI.PollerType
+    }
+    func stopAndRemovePoller(for server: String) { accept(args: [server]) }
+    func stopAndRemoveAllPollers() { accept() }
+    
     func getTimeSinceLastOpen(using dependencies: Dependencies) -> TimeInterval {
         return accept(args: [dependencies]) as! TimeInterval
     }
diff --git a/SessionMessagingKitTests/_TestUtilities/MockOGPoller.swift b/SessionMessagingKitTests/_TestUtilities/MockOGPoller.swift
new file mode 100644
index 000000000..4c7358b5d
--- /dev/null
+++ b/SessionMessagingKitTests/_TestUtilities/MockOGPoller.swift
@@ -0,0 +1,16 @@
+// Copyright © 2024 Rangeproof Pty Ltd. All rights reserved.
+
+import Foundation
+import SessionUtilitiesKit
+
+@testable import SessionMessagingKit
+
+class MockOGPoller: Mock<OpenGroupAPI.PollerType>, OpenGroupAPI.PollerType {
+    func startIfNeeded(using dependencies: Dependencies) {
+        accept()
+    }
+    
+    func stop() {
+        accept()
+    }
+}
diff --git a/SessionUtilitiesKit/Crypto/Mnemonic.swift b/SessionUtilitiesKit/Crypto/Mnemonic.swift
index a9dbcfd0c..9dd184e5e 100644
--- a/SessionUtilitiesKit/Crypto/Mnemonic.swift
+++ b/SessionUtilitiesKit/Crypto/Mnemonic.swift
@@ -118,7 +118,9 @@ public enum Mnemonic {
     }
     
     public static func decode(mnemonic: String, language: Language = .english) throws -> String {
-        var words: [String] = mnemonic.components(separatedBy: .whitespacesAndNewlines)
+        var words: [String] = mnemonic
+            .components(separatedBy: .whitespacesAndNewlines)
+            .map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
         let truncatedWordSet: [String] = language.loadTruncatedWordSet()
         let prefixLength: Int = language.prefixLength
         var result = ""
@@ -131,6 +133,13 @@ public enum Mnemonic {
         // Get checksum word
         let checksumWord = words.popLast()!
         
+        // Limit the words to a multiple of 3 to avoid index-out-of-bounds issues
+        let remainder: Int = (words.count % 3)
+        
+        if remainder > 0 {
+            words.removeLast(remainder)
+        }
+        
         // Decode
         for chunkStartIndex in stride(from: 0, to: words.count, by: 3) {
             guard
diff --git a/SessionUtilitiesKit/Database/Types/PagedDatabaseObserver.swift b/SessionUtilitiesKit/Database/Types/PagedDatabaseObserver.swift
index 7fa6df7e0..9b6f6dd3a 100644
--- a/SessionUtilitiesKit/Database/Types/PagedDatabaseObserver.swift
+++ b/SessionUtilitiesKit/Database/Types/PagedDatabaseObserver.swift
@@ -45,6 +45,9 @@ public class PagedDatabaseObserver<ObservedTable, T>: TransactionObserver where
     
     // MARK: - Initialization
     
+    /// Create a `PagedDatabaseObserver` which triggers the callback whenever changes occur
+    ///
+    /// **Note:** The `onChangeUnsorted` could be run on any logic may need to be shifted to the UI thread
     public init(
         pagedTable: ObservedTable.Type,
         pageSize: Int,
@@ -766,18 +769,10 @@ public class PagedDatabaseObserver<ObservedTable, T>: TransactionObserver where
         self.dataCache.mutate { $0 = $0.upserting(items: associatedLoadedData.values) }
         self.pageInfo.mutate { $0 = updatedPageInfo }
         
-        let triggerUpdates: () -> () = { [weak self, dataCache = self.dataCache.wrappedValue] in
-            self?.onChangeUnsorted(dataCache.values, updatedPageInfo)
-            self?.isLoadingMoreData.mutate { $0 = false }
-        }
-        
-        // Make sure the updates run on the main thread
-        guard Thread.isMainThread else {
-            DispatchQueue.main.async { triggerUpdates() }
-            return
-        }
-        
-        triggerUpdates()
+        // Trigger the unsorted change callback (the actual UI update triggering should eventually be run on
+        // the main thread via the `PagedData.processAndTriggerUpdates` function)
+        self.onChangeUnsorted(self.dataCache.wrappedValue.values, updatedPageInfo)
+        self.isLoadingMoreData.mutate { $0 = false }
     }
     
     public func reload() {
@@ -1022,7 +1017,7 @@ public enum PagedData {
     public static func processAndTriggerUpdates<SectionModel: DifferentiableSection>(
         updatedData: [SectionModel]?,
         currentDataRetriever: @escaping (() -> [SectionModel]?),
-        onDataChange: (([SectionModel], StagedChangeset<[SectionModel]>) -> ())?,
+        onDataChangeRetriever: @escaping (() -> (([SectionModel], StagedChangeset<[SectionModel]>) -> ())?),
         onUnobservedDataChange: @escaping (([SectionModel], StagedChangeset<[SectionModel]>) -> Void)
     ) {
         guard let updatedData: [SectionModel] = updatedData else { return }
@@ -1045,7 +1040,7 @@ public enum PagedData {
             ///
             /// **Note:** We do this even if the 'changeset' is empty because if this change reverts a previous change we
             /// need to ensure the `onUnobservedDataChange` gets cleared so it doesn't end up in an invalid state
-            guard let onDataChange: (([SectionModel], StagedChangeset<[SectionModel]>) -> ()) = onDataChange else {
+            guard let onDataChange: (([SectionModel], StagedChangeset<[SectionModel]>) -> ()) = onDataChangeRetriever() else {
                 onUnobservedDataChange(updatedData, changeset)
                 return
             }
@@ -1056,7 +1051,7 @@ public enum PagedData {
             onDataChange(updatedData, changeset)
         }
         
-        // No need to dispatch to the next run loop if we are alread on the main thread
+        // No need to dispatch to the next run loop if we are already on the main thread
         guard !Thread.isMainThread else {
             performUpdates()
             return
@@ -1096,7 +1091,7 @@ public enum PagedData {
             valueSubject?.send((updatedData, StagedChangeset()))
         }
         
-        // No need to dispatch to the next run loop if we are alread on the main thread
+        // No need to dispatch to the next run loop if we are already on the main thread
         guard !Thread.isMainThread else {
             performUpdates()
             return