// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved. import Combine import GRDB @testable import SessionUtilitiesKit class SynchronousStorage: Storage { public init( customWriter: DatabaseWriter? = nil, migrationTargets: [MigratableTarget.Type]? = nil, migrations: [Storage.KeyedMigration]? = nil, initialData: ((Database) throws -> ())? = nil ) { super.init(customWriter: customWriter) // Process any migration targets first if let migrationTargets: [MigratableTarget.Type] = migrationTargets { perform( migrationTargets: migrationTargets, async: false, onProgressUpdate: nil, onMigrationRequirement: { _, _ in }, onComplete: { _, _ in } ) } // Then process any provided migration info if let migrations: [Storage.KeyedMigration] = migrations { perform( sortedMigrations: migrations, async: false, onProgressUpdate: nil, onMigrationRequirement: { _, _ in }, onComplete: { _, _ in } ) } write { db in try initialData?(db) } } @discardableResult override func write( fileName: String = #file, functionName: String = #function, lineNumber: Int = #line, using dependencies: Dependencies = Dependencies(), updates: @escaping (Database) throws -> T? ) -> T? { guard isValid, let dbWriter: DatabaseWriter = testDbWriter else { return nil } // If 'forceSynchronous' is true then it's likely that we will access the database in // a reentrant way, the 'unsafeReentrant...' functions allow us to interact with the // database without worrying about reentrant access during tests because we can be // confident that the tests are running on the correct thread guard !dependencies.forceSynchronous else { return try? dbWriter.unsafeReentrantWrite(updates) } return super.write( fileName: fileName, functionName: functionName, lineNumber: lineNumber, using: dependencies, updates: updates ) } @discardableResult override func read( using dependencies: Dependencies = Dependencies(), _ value: (Database) throws -> T? ) -> T? { guard isValid, let dbWriter: DatabaseWriter = testDbWriter else { return nil } // If 'forceSynchronous' is true then it's likely that we will access the database in // a reentrant way, the 'unsafeReentrant...' functions allow us to interact with the // database without worrying about reentrant access during tests because we can be // confident that the tests are running on the correct thread guard !dependencies.forceSynchronous else { return try? dbWriter.unsafeReentrantRead(value) } return super.read(using: dependencies, value) } // MARK: - Async Methods override func readPublisher( using dependencies: Dependencies = Dependencies(), value: @escaping (Database) throws -> T ) -> AnyPublisher { guard let result: T = self.read(using: dependencies, value) else { return Fail(error: StorageError.generic) .eraseToAnyPublisher() } return Just(result) .setFailureType(to: Error.self) .eraseToAnyPublisher() } override func writeAsync( fileName: String = #file, functionName: String = #function, lineNumber: Int = #line, using dependencies: Dependencies = Dependencies(), updates: @escaping (Database) throws -> T, completion: @escaping (Database, Result) throws -> Void ) { do { let result: T = try write(using: dependencies, updates: updates) ?? { throw StorageError.failedToSave }() write { db in try completion(db, Result.success(result)) } } catch { write { db in try completion(db, Result.failure(error)) } } } override func writePublisher( fileName: String = #file, functionName: String = #function, lineNumber: Int = #line, using dependencies: Dependencies = Dependencies(), updates: @escaping (Database) throws -> T ) -> AnyPublisher { guard let result: T = super.write(fileName: fileName, functionName: functionName, lineNumber: lineNumber, using: dependencies, updates: updates) else { return Fail(error: StorageError.generic) .eraseToAnyPublisher() } return Just(result) .setFailureType(to: Error.self) .eraseToAnyPublisher() } }