Skip to content

Commit

Permalink
[Core] Make storage class conform to Sendable
Browse files Browse the repository at this point in the history
  • Loading branch information
ncooke3 committed Oct 21, 2024
1 parent 8328630 commit 597de2b
Show file tree
Hide file tree
Showing 4 changed files with 37 additions and 47 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -17,19 +17,20 @@ import Foundation
/// A type that can perform atomic operations using block-based transformations.
protocol HeartbeatStorageProtocol {
func readAndWriteSync(using transform: (HeartbeatsBundle?) -> HeartbeatsBundle?)
func readAndWriteAsync(using transform: @escaping (HeartbeatsBundle?) -> HeartbeatsBundle?)
func readAndWriteAsync(using transform: @escaping @Sendable (HeartbeatsBundle?)
-> HeartbeatsBundle?)
func getAndSet(using transform: (HeartbeatsBundle?) -> HeartbeatsBundle?) throws
-> HeartbeatsBundle?
func getAndSetAsync(using transform: @escaping (HeartbeatsBundle?) -> HeartbeatsBundle?,
completion: @escaping (Result<HeartbeatsBundle?, Error>) -> Void)
func getAndSetAsync(using transform: @escaping @Sendable (HeartbeatsBundle?) -> HeartbeatsBundle?,
completion: @escaping @Sendable (Result<HeartbeatsBundle?, Error>) -> Void)
}

/// Thread-safe storage object designed for transforming heartbeat data that is persisted to disk.
final class HeartbeatStorage: HeartbeatStorageProtocol {
final class HeartbeatStorage: Sendable, HeartbeatStorageProtocol {
/// The identifier used to differentiate instances.
private let id: String
/// The underlying storage container to read from and write to.
private let storage: Storage
private let storage: any Storage
/// The encoder used for encoding heartbeat data.
private let encoder: JSONEncoder = .init()
/// The decoder used for decoding heartbeat data.
Expand Down Expand Up @@ -107,7 +108,8 @@ final class HeartbeatStorage: HeartbeatStorageProtocol {
/// Asynchronously reads from and writes to storage using the given transform block.
/// - Parameter transform: A block to transform the currently stored heartbeats bundle to a new
/// heartbeats bundle value.
func readAndWriteAsync(using transform: @escaping (HeartbeatsBundle?) -> HeartbeatsBundle?) {
func readAndWriteAsync(using transform: @escaping @Sendable (HeartbeatsBundle?)
-> HeartbeatsBundle?) {
queue.async { [self] in
let oldHeartbeatsBundle = try? load(from: storage)
let newHeartbeatsBundle = transform(oldHeartbeatsBundle)
Expand Down Expand Up @@ -143,8 +145,8 @@ final class HeartbeatStorage: HeartbeatStorageProtocol {
/// - completion: An escaping block used to process the heartbeat data that
/// was stored (before the `transform` was applied); otherwise, the error
/// that occurred.
func getAndSetAsync(using transform: @escaping (HeartbeatsBundle?) -> HeartbeatsBundle?,
completion: @escaping (Result<HeartbeatsBundle?, Error>) -> Void) {
func getAndSetAsync(using transform: @escaping @Sendable (HeartbeatsBundle?) -> HeartbeatsBundle?,
completion: @escaping @Sendable (Result<HeartbeatsBundle?, Error>) -> Void) {
queue.async {
do {
let oldHeartbeatsBundle = try? self.load(from: self.storage)
Expand Down
29 changes: 17 additions & 12 deletions FirebaseCore/Internal/Sources/HeartbeatLogging/Storage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
import Foundation

/// A type that reads from and writes to an underlying storage container.
protocol Storage {
protocol Storage: Sendable {
/// Reads and returns the data stored by this storage type.
/// - Returns: The data read from storage.
/// - Throws: An error if the read failed.
Expand All @@ -38,16 +38,12 @@ enum StorageError: Error {
final class FileStorage: Storage {
/// A file system URL to the underlying file resource.
private let url: URL
/// The file manager used to perform file system operations.
private let fileManager: FileManager

/// Designated initializer.
/// - Parameters:
/// - url: A file system URL for the underlying file resource.
/// - fileManager: A file manager. Defaults to `default` manager.
init(url: URL, fileManager: FileManager = .default) {
init(url: URL) {
self.url = url
self.fileManager = fileManager
}

/// Reads and returns the data from this object's associated file resource.
Expand Down Expand Up @@ -90,7 +86,7 @@ final class FileStorage: Storage {
/// - Parameter url: The URL to create directories in.
private func createDirectories(in url: URL) throws {
do {
try fileManager.createDirectory(
try FileManager.default.createDirectory(
at: url,
withIntermediateDirectories: true
)
Expand All @@ -104,17 +100,26 @@ final class FileStorage: Storage {

/// A object that provides API for reading and writing to a user defaults resource.
final class UserDefaultsStorage: Storage {
/// The underlying defaults container.
private let defaults: UserDefaults
/// The suite name for the underlying defaults container.
private let suiteName: String

/// The key mapping to the object's associated resource in `defaults`.
private let key: String

/// The underlying defaults container.
private var defaults: UserDefaults {
// It's safe to force unwrap the below defaults instance because the
// initializer only returns `nil` when the bundle id or `globalDomain`
// is passed in as the `suiteName`.
UserDefaults(suiteName: suiteName)!
}

/// Designated initializer.
/// - Parameters:
/// - defaults: The defaults container.
/// - suiteName: The suite name for the defaults container.
/// - key: The key mapping to the value stored in the defaults container.
init(defaults: UserDefaults, key: String) {
self.defaults = defaults
init(suiteName: String, key: String) {
self.suiteName = suiteName
self.key = key
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,11 +56,7 @@ extension FileManager {
extension UserDefaultsStorage: StorageFactory {
static func makeStorage(id: String) -> Storage {
let suiteName = Constants.heartbeatUserDefaultsSuiteName
// It's safe to force unwrap the below defaults instance because the
// initializer only returns `nil` when the bundle id or `globalDomain`
// is passed in as the `suiteName`.
let defaults = UserDefaults(suiteName: suiteName)!
let key = "heartbeats-\(id)"
return UserDefaultsStorage(defaults: defaults, key: key)
return UserDefaultsStorage(suiteName: suiteName, key: key)
}
}
31 changes: 9 additions & 22 deletions FirebaseCore/Internal/Tests/Unit/StorageTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -97,22 +97,23 @@ class FileStorageTests: XCTestCase {

class UserDefaultsStorageTests: XCTestCase {
var defaults: UserDefaults!
let suiteName = #file
let suiteName = "com.firebase.userdefaults.storageTests"

override func setUpWithError() throws {
defaults = try XCTUnwrap(UserDefaultsFake(suiteName: suiteName))
// Clear the user default suite before testing.
UserDefaults(suiteName: suiteName)?.removePersistentDomain(forName: suiteName)
}

func testRead_WhenDefaultDoesNotExist_ThrowsError() throws {
// Given
let defaultsStorage = UserDefaultsStorage(defaults: defaults, key: #function)
let defaultsStorage = UserDefaultsStorage(suiteName: suiteName, key: #function)
// Then
XCTAssertThrowsError(try defaultsStorage.read())
}

func testRead_WhenDefaultExists_ReturnsDefault() throws {
// Given
let defaultsStorage = UserDefaultsStorage(defaults: defaults, key: #function)
let defaultsStorage = UserDefaultsStorage(suiteName: suiteName, key: #function)
XCTAssertNoThrow(try defaultsStorage.write(Constants.testData))
// When
let storedData = try defaultsStorage.read()
Expand All @@ -122,7 +123,7 @@ class UserDefaultsStorageTests: XCTestCase {

func testWriteData_WhenDefaultDoesNotExist_CreatesDefault() throws {
// Given
let defaultsStorage = UserDefaultsStorage(defaults: defaults, key: #function)
let defaultsStorage = UserDefaultsStorage(suiteName: suiteName, key: #function)
XCTAssertThrowsError(try defaultsStorage.read())
// When
XCTAssertNoThrow(try defaultsStorage.write(Constants.testData))
Expand All @@ -133,7 +134,7 @@ class UserDefaultsStorageTests: XCTestCase {

func testWriteData_WhenDefaultExists_ModifiesDefault() throws {
// Given
let defaultsStorage = UserDefaultsStorage(defaults: defaults, key: #function)
let defaultsStorage = UserDefaultsStorage(suiteName: suiteName, key: #function)
XCTAssertNoThrow(try defaultsStorage.write(Constants.testData))
// When
let modifiedData = #function.data(using: .utf8)
Expand All @@ -146,7 +147,7 @@ class UserDefaultsStorageTests: XCTestCase {

func testWriteNil_WhenDefaultDoesNotExist_RemovesDefault() throws {
// Given
let defaultsStorage = UserDefaultsStorage(defaults: defaults, key: #function)
let defaultsStorage = UserDefaultsStorage(suiteName: suiteName, key: #function)
XCTAssertThrowsError(try defaultsStorage.read())
// When
XCTAssertNoThrow(try defaultsStorage.write(nil))
Expand All @@ -156,25 +157,11 @@ class UserDefaultsStorageTests: XCTestCase {

func testWriteNil_WhenDefaultExists_RemovesDefault() throws {
// Given
let defaultsStorage = UserDefaultsStorage(defaults: defaults, key: #function)
let defaultsStorage = UserDefaultsStorage(suiteName: suiteName, key: #function)
XCTAssertNoThrow(try defaultsStorage.write(Constants.testData))
// When
XCTAssertNoThrow(try defaultsStorage.write(nil))
// Then
XCTAssertThrowsError(try defaultsStorage.read())
}
}

// MARK: - Fakes

private class UserDefaultsFake: UserDefaults {
private var defaults = [String: Any]()

override func object(forKey defaultName: String) -> Any? {
defaults[defaultName]
}

override func set(_ value: Any?, forKey defaultName: String) {
defaults[defaultName] = value
}
}

0 comments on commit 597de2b

Please sign in to comment.