Swift 6 migration for NSE (#5608)

* swift 6 support for NSE and concurrency safe implementations for it

* enforcing required Sendable conformance for swift 6 migration in NSE

* use Mutex to access the string builder cache

* use an async stream to update timeline items diffs

* fix for Sendable appHooks
This commit is contained in:
Mauro
2026-05-21 20:21:57 +02:00
committed by GitHub
parent 91ccff36fc
commit 2462ddea85
20 changed files with 226 additions and 150 deletions
@@ -10,7 +10,9 @@ import Combine
/// A wrapper of CurrentValueSubject.
/// The purpose of this type is to remove the possibility to send new values on the underlying subject.
struct CurrentValuePublisher<Output, Failure: Error>: Publisher {
///
/// `CurrentValueSubject` is documented as thread-safe but is not formally `Sendable`, hence `@unchecked`.
struct CurrentValuePublisher<Output, Failure: Error>: Publisher, @unchecked Sendable {
private let subject: CurrentValueSubject<Output, Failure>
init(_ subject: CurrentValueSubject<Output, Failure>) {
@@ -15,9 +15,9 @@ enum ExpiringTaskRunnerError: Error {
actor ExpiringTaskRunner<T: Sendable> {
private var continuation: CheckedContinuation<T, Error>?
private var task: () async throws -> T
init(_ task: @escaping () async throws -> T) {
private var task: @Sendable () async throws -> T
init(_ task: @escaping @Sendable () async throws -> T) {
self.task = task
}
+16 -22
View File
@@ -7,6 +7,7 @@
//
import Foundation
import Synchronization
public extension Bundle {
/// The top-level bundle that contains the entire app.
@@ -24,12 +25,12 @@ public extension Bundle {
// MARK: - Localisation
/// Overrides `Bundle.app.preferredLocalizations` for testing translations.
static var overrideLocalizations: [String]?
private static let cacheDispatchQueue = DispatchQueue(label: "io.element.elementx.localization_bundle_cache")
private static var cachedBundles = [String: Bundle]()
/// Overrides `Bundle.app.preferredLocalizations` for testing translations since this is
/// only for testing, and is changed at runtime only in tests, it's fine to keep as `nonisolated(unsafe)`
nonisolated(unsafe) static var overrideLocalizations: [String]?
private static let cachedBundles = Mutex<[String: Bundle]>([:])
/// Get an lproj language bundle from the receiver bundle.
/// - Parameter language: The language to try to load.
/// - Returns: The lproj bundle if found otherwise nil.
@@ -37,32 +38,25 @@ public extension Bundle {
if let bundle = cachedValue(forKey: language) {
return bundle
}
guard let lprojURL = Bundle.app.url(forResource: language, withExtension: "lproj") else {
return nil
}
let bundle = Bundle(url: lprojURL)
cacheValue(bundle, forKey: language)
return bundle
}
// MARK: - Private
private static func cacheValue(_ value: Bundle?, forKey key: String) {
cacheDispatchQueue.sync {
cachedBundles[key] = value
}
cachedBundles.withLock { $0[key] = value }
}
private static func cachedValue(forKey key: String) -> Bundle? {
var result: Bundle?
cacheDispatchQueue.sync {
result = cachedBundles[key]
}
return result
cachedBundles.withLock { $0[key] }
}
}
@@ -10,6 +10,7 @@ import Compound
import LRUCache
import MatrixRustSDK
import SwiftSoup
import Synchronization
import UIKit
protocol MentionBuilderProtocol {
@@ -42,11 +43,10 @@ struct AttributedStringBuilder: AttributedStringBuilderProtocol {
private let mentionBuilder: MentionBuilderProtocol
private static let attributeMSC4286 = "msc4286-external-payment-details"
private static let cacheDispatchQueue = DispatchQueue(label: "io.element.elementx.attributed_string_builder_v2_cache")
private static var caches: [String: LRUCache<String, AttributedString>] = [:]
private static let caches = Mutex<[String: LRUCache<String, AttributedString>]>([:])
static func invalidateCaches() {
caches.removeAll()
caches.withLock { $0.removeAll() }
}
init(cacheKey: String = defaultKey, mentionBuilder: MentionBuilderProtocol) {
@@ -279,22 +279,17 @@ struct AttributedStringBuilder: AttributedStringBuilderProtocol {
}
private static func cacheValue(_ value: AttributedString?, forKey key: String, cacheKey: String) {
cacheDispatchQueue.sync {
caches.withLock { caches in
if caches[cacheKey] == nil {
caches[cacheKey] = LRUCache<String, AttributedString>(countLimit: 1000)
}
caches[cacheKey]?.setValue(value, forKey: key)
}
}
private static func cachedValue(forKey key: String, cacheKey: String) -> AttributedString? {
var result: AttributedString?
cacheDispatchQueue.sync {
result = caches[cacheKey]?.value(forKey: key)
}
return result
caches.withLock { $0[cacheKey]?.value(forKey: key) }
}
// swiftlint:disable:next cyclomatic_complexity
+1 -1
View File
@@ -22,7 +22,7 @@ enum Tracing {
/// Set this to temporarily override the directory from which logs will be collected.
/// This basically only affects ``logFiles``, and doesn't inform the SDK to write
/// the logs to a different directory, which should be done before setting this.
static var logsDirectoryOverride: URL?
nonisolated(unsafe) static var logsDirectoryOverride: URL?
static var legacyLogsDirectory: URL {
.appGroupContainerDirectory
}
+4 -4
View File
@@ -13,12 +13,12 @@ import MatrixRustSDK
///
/// To use this you'll need to add a conformance to the required listener
/// protocol with a specialisation for the type it listens for.
final class SDKListener<T> {
private let onUpdateClosure: (T) -> Void
final class SDKListener<T>: Sendable {
private let onUpdateClosure: @Sendable (T) -> Void
/// Creates a new listener.
/// - Parameter onUpdateClosure: A closure that will be called whenever a new value is available.
init(_ onUpdateClosure: @escaping (T) -> Void) {
init(_ onUpdateClosure: @escaping @Sendable (T) -> Void) {
self.onUpdateClosure = onUpdateClosure
}
}