diff --git a/Sources/ProcessOut/Sources/Core/Formatters/PhoneNumber/MetadataProvider/DefaultPhoneNumberMetadataProvider.swift b/Sources/ProcessOut/Sources/Core/Formatters/PhoneNumber/MetadataProvider/DefaultPhoneNumberMetadataProvider.swift index 942a42fdb..4d85408ea 100644 --- a/Sources/ProcessOut/Sources/Core/Formatters/PhoneNumber/MetadataProvider/DefaultPhoneNumberMetadataProvider.swift +++ b/Sources/ProcessOut/Sources/Core/Formatters/PhoneNumber/MetadataProvider/DefaultPhoneNumberMetadataProvider.swift @@ -11,36 +11,55 @@ final class DefaultPhoneNumberMetadataProvider: PhoneNumberMetadataProvider { static let shared = DefaultPhoneNumberMetadataProvider() + /// - NOTE: Method is asynchronous. + func prewarm() { + loadMetadata(sync: false) + } + // MARK: - PhoneNumberMetadataProvider func metadata(for countryCode: String) -> PhoneNumberMetadata? { let transformedCountryCode = countryCode.applyingTransform(.toLatin, reverse: false) ?? countryCode - return metadata[transformedCountryCode] + if let metadata = metadata { + return metadata[transformedCountryCode] + } + loadMetadata(sync: true) + return metadata?[transformedCountryCode] } - func prewarm() { - _ = metadata - } + // MARK: - Private Properties + + private let dispatchQueue: DispatchQueue + + @UnfairlyLocked + private var metadata: [String: PhoneNumberMetadata]? // MARK: - Private Methods private init() { - // NOP + dispatchQueue = DispatchQueue(label: "process-out.phone-number-metadata-provider", qos: .userInitiated) } - // MARK: - Private Properties - - private lazy var metadata: [String: PhoneNumberMetadata] = { - let metadata: [PhoneNumberMetadata] - do { - let data = try Data(contentsOf: Files.phoneNumberMetadata.url) - metadata = try JSONDecoder().decode([PhoneNumberMetadata].self, from: data) - } catch { - return [:] - } - return Dictionary(grouping: metadata, by: \.countryCode).compactMapValues { values in - let countryCode = values.first!.countryCode // swiftlint:disable:this force_unwrapping - return PhoneNumberMetadata(countryCode: countryCode, formats: values.flatMap(\.formats)) + private func loadMetadata(sync: Bool) { + let dispatchWorkItem = DispatchWorkItem { [weak self] in + guard let self, self.metadata == nil else { + return + } + let groupedMetadata: [String: PhoneNumberMetadata] + do { + let data = try Data(contentsOf: Files.phoneNumberMetadata.url) + let metadata = try JSONDecoder().decode([PhoneNumberMetadata].self, from: data) + groupedMetadata = Dictionary(grouping: metadata, by: \.countryCode).compactMapValues { values in + let countryCode = values.first!.countryCode // swiftlint:disable:this force_unwrapping + return PhoneNumberMetadata(countryCode: countryCode, formats: values.flatMap(\.formats)) + } + } catch { + assertionFailure("Failed to load metadata: \(error.localizedDescription)") + groupedMetadata = [:] + } + self.$metadata.withLock { $0 = groupedMetadata } } - }() + let executor = sync ? dispatchQueue.sync : dispatchQueue.async + executor(dispatchWorkItem) + } } diff --git a/Sources/ProcessOut/Sources/Core/Logger/Destinations/SystemLoggerDestination.swift b/Sources/ProcessOut/Sources/Core/Logger/Destinations/SystemLoggerDestination.swift index 33a2a46f6..f7b5a5179 100644 --- a/Sources/ProcessOut/Sources/Core/Logger/Destinations/SystemLoggerDestination.swift +++ b/Sources/ProcessOut/Sources/Core/Logger/Destinations/SystemLoggerDestination.swift @@ -6,7 +6,7 @@ // import Foundation -import os +@_implementationOnly import os final class SystemLoggerDestination: LoggerDestination { diff --git a/Sources/ProcessOut/Sources/Core/Utils/UnfairlyLocked.swift b/Sources/ProcessOut/Sources/Core/Utils/UnfairlyLocked.swift new file mode 100644 index 000000000..9a9409e39 --- /dev/null +++ b/Sources/ProcessOut/Sources/Core/Utils/UnfairlyLocked.swift @@ -0,0 +1,63 @@ +// +// UnfairlyLocked.swift +// ProcessOut +// +// Created by Andrii Vysotskyi on 14.07.2023. +// + +@_implementationOnly import os + +/// A thread-safe wrapper around a value. +@propertyWrapper +final class UnfairlyLocked { + + init(wrappedValue: Value) { + value = wrappedValue + } + + /// The contained value. Unsafe for anything more than direct read or write. + var wrappedValue: Value { + lock.withLock { value } + } + + var projectedValue: UnfairlyLocked { + self + } + + func withLock(_ body: (inout Value) -> R) -> R { + lock.withLock { + body(&value) + } + } + + // MARK: - Private Properties + + private let lock = UnfairLock() + private var value: Value +} + +/// An `os_unfair_lock` wrapper. +private final class UnfairLock { + + init() { + unfairLock = .allocate(capacity: 1) + unfairLock.initialize(to: os_unfair_lock()) + } + + func withLock(_ body: () -> R) -> R { + defer { + os_unfair_lock_unlock(unfairLock) + } + os_unfair_lock_lock(unfairLock) + return body() + } + + deinit { + unfairLock.deinitialize(count: 1) + unfairLock.deallocate() + } + + // MARK: - Private Properties + + private let unfairLock: os_unfair_lock_t +}