226 lines
8.5 KiB
Swift
226 lines
8.5 KiB
Swift
//
|
|
// Copyright 2023 Signal Messenger, LLC
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
//
|
|
|
|
import Foundation
|
|
|
|
public extension SDSAnyReadTransaction {
|
|
/// Bridging from a SDS transaction to a DB transaction can be done at the seams;
|
|
/// when you have an old class using SDS classes talking to a new one using DB classes.
|
|
var asV2Read: DBReadTransaction { return SDSDB.ReadTx(self) }
|
|
}
|
|
|
|
public extension SDSAnyWriteTransaction {
|
|
/// Bridging from a SDS transaction to a DB transaction can be done at the seams;
|
|
/// when you have an old class using SDS classes talking to a new one using DB classes.
|
|
var asV2Write: DBWriteTransaction { return SDSDB.WriteTx(self) }
|
|
}
|
|
|
|
/// A perfectly transparent proxy to `SDSDatabaseStorage`. Uses V2 types instead.
|
|
///
|
|
/// Classes using `DB` can simply interact with that protocol the same way in both
|
|
/// production code and tests, producing transaction objects which they pass down to
|
|
/// lower-level classes which perform database operations.
|
|
/// In production code, the transaction instances will be produced by this class, get converted
|
|
/// at some point before actually being used with `shimOnlyBridge` methods, and everything
|
|
/// will be fine.
|
|
/// In tests, the transactions will be stubs, and will crash (failing tests) if they ever are passed to
|
|
/// `shimOnlyBridge` methods. This means you _have_ to stub out all your db operations
|
|
/// within test setup code; if you don't and hit an actual db operation code path, your test will fail.
|
|
/// This is a good thing; it helps you ensure your tests are scoped explicitly (what isn't explicitly
|
|
/// mocked doesn't work!), and ensures your tests aren't subject to random external behavior changes.
|
|
public class SDSDB: DB {
|
|
|
|
fileprivate final class ReadTx: DBReadTransaction {
|
|
fileprivate let tx: SDSAnyReadTransaction
|
|
init(_ tx: SDSAnyReadTransaction) { self.tx = tx }
|
|
}
|
|
|
|
fileprivate final class WriteTx: DBWriteTransaction {
|
|
fileprivate let tx: SDSAnyWriteTransaction
|
|
init(_ tx: SDSAnyWriteTransaction) { self.tx = tx }
|
|
|
|
func addSyncCompletion(_ block: @escaping () -> Void) {
|
|
tx.addSyncCompletion(block)
|
|
}
|
|
|
|
func addAsyncCompletion(on scheduler: Scheduler, _ block: @escaping () -> Void) {
|
|
tx.addAsyncCompletion(on: scheduler, block: block)
|
|
}
|
|
}
|
|
|
|
// MARK: - Bridging
|
|
|
|
public static func shimOnlyBridge(_ readTx: DBReadTransaction) -> SDSAnyReadTransaction {
|
|
// This is why `ReadTx` should be the **ONLY** concrete implementor of DBReadTransaction
|
|
// in production code.
|
|
// A stub test transaction will crash if it ever gets here; this is good because if it got
|
|
// here in a test it means you didn't sub out the things that actually need an SDS transaction
|
|
// and talk to the db.
|
|
if let write = readTx as? WriteTx {
|
|
return write.tx
|
|
}
|
|
return (readTx as! ReadTx).tx
|
|
}
|
|
|
|
public static func shimOnlyBridge(_ writeTx: DBWriteTransaction) -> SDSAnyWriteTransaction {
|
|
// This is why `WriteTx` should be the **ONLY** concrete implementor of DBWriteTransaction
|
|
// in production code.
|
|
// A stub test transaction will crash if it ever gets here; this is good because if it got
|
|
// here in a test it means you didn't sub out the things that actually need an SDS transaction
|
|
// and talk to the db.
|
|
return (writeTx as! WriteTx).tx
|
|
}
|
|
|
|
// MARK: - Init
|
|
|
|
private let databaseStorage: SDSDatabaseStorage
|
|
|
|
public init(databaseStorage: SDSDatabaseStorage) {
|
|
self.databaseStorage = databaseStorage
|
|
}
|
|
|
|
// MARK: Async Methods
|
|
|
|
public func asyncRead<T>(
|
|
file: String = #file,
|
|
function: String = #function,
|
|
line: Int = #line,
|
|
block: @escaping (DBReadTransaction) -> T,
|
|
completionQueue: DispatchQueue = .main,
|
|
completion: ((T) -> Void)? = nil
|
|
) {
|
|
databaseStorage.asyncRead(file: file, function: function, line: line, block: {block(ReadTx($0))}, completionQueue: completionQueue, completion: completion)
|
|
}
|
|
|
|
public func asyncWrite<T>(
|
|
file: String = #file,
|
|
function: String = #function,
|
|
line: Int = #line,
|
|
block: @escaping (DBWriteTransaction) -> T,
|
|
completionQueue: DispatchQueue = .main,
|
|
completion: ((T) -> Void)? = nil
|
|
) {
|
|
databaseStorage.asyncWrite(file: file, function: function, line: line, block: {block(WriteTx($0))}, completionQueue: completionQueue, completion: completion)
|
|
}
|
|
|
|
// MARK: Awaitable Methods
|
|
|
|
public func awaitableWrite<T>(
|
|
file: String = #file,
|
|
function: String = #function,
|
|
line: Int = #line,
|
|
block: @escaping (DBWriteTransaction) throws -> T
|
|
) async rethrows -> T {
|
|
return try await databaseStorage.awaitableWrite(file: file, function: function, line: line, block: {try block(WriteTx($0))})
|
|
}
|
|
|
|
// MARK: Promises
|
|
|
|
public func readPromise<T>(
|
|
file: String = #file,
|
|
function: String = #function,
|
|
line: Int = #line,
|
|
_ block: @escaping (DBReadTransaction) throws -> T
|
|
) -> Promise<T> {
|
|
return databaseStorage.read(.promise, file: file, function: function, line: line, {try block(ReadTx($0))})
|
|
}
|
|
|
|
public func writePromise<T>(
|
|
file: String = #file,
|
|
function: String = #function,
|
|
line: Int = #line,
|
|
_ block: @escaping (DBWriteTransaction) throws -> T
|
|
) -> Promise<T> {
|
|
return databaseStorage.write(.promise, file: file, function: function, line: line, {try block(WriteTx($0))})
|
|
}
|
|
|
|
// MARK: Value Methods
|
|
|
|
public func read<T>(
|
|
file: String = #file,
|
|
function: String = #function,
|
|
line: Int = #line,
|
|
block: (DBReadTransaction) throws -> T
|
|
) rethrows -> T {
|
|
return try databaseStorage.read(file: file, function: function, line: line, block: {try block(ReadTx($0))})
|
|
}
|
|
|
|
public func write<T>(
|
|
file: String = #file,
|
|
function: String = #function,
|
|
line: Int = #line,
|
|
block: (DBWriteTransaction) throws -> T
|
|
) rethrows -> T {
|
|
return try databaseStorage.write(file: file, function: function, line: line, block: {try block(WriteTx($0))})
|
|
}
|
|
|
|
// MARK: - Observation
|
|
|
|
public func appendDbChangeDelegate(_ dbChangeDelegate: DBChangeDelegate) {
|
|
self.databaseStorage.appendDatabaseChangeDelegate(DBChangeDelegateWrapper(dbChangeDelegate))
|
|
}
|
|
|
|
// MARK: - Touching
|
|
|
|
public func touch(_ interaction: TSInteraction, shouldReindex: Bool, tx: DBWriteTransaction) {
|
|
self.databaseStorage.touch(interaction: interaction, shouldReindex: shouldReindex, transaction: SDSDB.shimOnlyBridge(tx))
|
|
}
|
|
|
|
public func touch(_ thread: TSThread, shouldReindex: Bool, shouldUpdateChatListUi: Bool, tx: DBWriteTransaction) {
|
|
self.databaseStorage.touch(
|
|
thread: thread,
|
|
shouldReindex: shouldReindex,
|
|
shouldUpdateChatListUi: shouldUpdateChatListUi,
|
|
transaction: SDSDB.shimOnlyBridge(tx)
|
|
)
|
|
}
|
|
|
|
public func touch(_ storyMessage: StoryMessage, tx: DBWriteTransaction) {
|
|
self.databaseStorage.touch(storyMessage: storyMessage, transaction: SDSDB.shimOnlyBridge(tx))
|
|
}
|
|
}
|
|
|
|
extension SDSAnyWriteTransaction {
|
|
var asRead: SDSAnyReadTransaction { self }
|
|
}
|
|
|
|
private class DBChangeDelegateWrapper: NSObject, DatabaseChangeDelegate {
|
|
|
|
/// Retains a strong reference to self intentionally, to avoid observation being lost when this object
|
|
/// is deallocated.
|
|
/// This reference is released if we ever find the dbChangeDelegate has been deallocated.
|
|
/// This isn't perfect, as this object can persist indefinitely if no callbacks happen, but presumably
|
|
/// the DatabaseChangeDelegate itself checks for deallocation on callbacks so...is it any worse?
|
|
private var strongSelf: Any?
|
|
private weak var dbChangeDelegate: DBChangeDelegate?
|
|
|
|
init(_ dbChangeDelegate: DBChangeDelegate) {
|
|
self.dbChangeDelegate = dbChangeDelegate
|
|
super.init()
|
|
strongSelf = self
|
|
}
|
|
|
|
func databaseChangesDidUpdate(databaseChanges: DatabaseChanges) {
|
|
checkRetain()
|
|
}
|
|
|
|
func databaseChangesDidUpdateExternally() {
|
|
checkRetain()?.dbChangesDidUpdateExternally()
|
|
}
|
|
|
|
func databaseChangesDidReset() {
|
|
checkRetain()
|
|
}
|
|
|
|
@discardableResult
|
|
private func checkRetain() -> DBChangeDelegate? {
|
|
guard let dbChangeDelegate else {
|
|
strongSelf = nil
|
|
return nil
|
|
}
|
|
return dbChangeDelegate
|
|
}
|
|
}
|