refactor(concurrency): isolate ProviderConfig to the main actor and drop unneeded @unchecked Sendable

- ProviderConfig is now @MainActor instead of @unchecked Sendable; AppL10n reads the persisted UI language key directly so off-main string lookups stay valid.
- EnglishLearningStore serializes its read-modify-write with a type-wide lock (several instances share one defaults key).
- URLSession-only transports, GatewayGrantKeychainStore and Keychain conform to Sendable without @unchecked. UserDefaults/DeviceCheck wrappers keep @unchecked because those SDK types are not Sendable.
This commit is contained in:
2026-10-20 23:00:12 +02:00
parent 9a4107cef8
commit d6b614b8be
13 changed files with 38 additions and 14 deletions
@@ -10,6 +10,10 @@ import Foundation
public final class EnglishLearningStore: @unchecked Sendable {
public static let defaultsKey = "englishTyping.learnedBoosts.v1"
/// Static because several instances share the same defaults key; a
/// per-instance lock would not serialize their read-modify-write.
private static let mutationLock = NSLock()
private let defaults: UserDefaults
public init(defaults: UserDefaults = AppGroupStore().defaults) {
@@ -42,8 +46,10 @@ public final class EnglishLearningStore: @unchecked Sendable {
private func mutate(word: String, delta: Int) {
let key = word.lowercased()
guard !key.isEmpty else { return }
var map = snapshot()
map[key] = min(10_000, (map[key] ?? 0) + delta)
defaults.set(map, forKey: Self.defaultsKey)
Self.mutationLock.withLock {
var map = snapshot()
map[key] = min(10_000, (map[key] ?? 0) + delta)
defaults.set(map, forKey: Self.defaultsKey)
}
}
}