You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
session-ios/SessionUtilitiesKit/Database/Utilities/Database+Utilities.swift

152 lines
5.2 KiB
Swift

// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
//
// stringlint:disable
import Foundation
import GRDB
// MARK: - Cache
internal extension Cache {
static let transactionObserver: CacheConfig<TransactionObserverCacheType, TransactionObserverImmutableCacheType> = Dependencies.create(
identifier: "transactionObserver",
createInstance: { dependencies in Storage.TransactionObserverCache(using: dependencies) },
mutableInstance: { $0 },
immutableInstance: { $0 }
)
}
public extension Database {
func makeFTS5Pattern<T>(rawPattern: String, forTable table: T.Type) throws -> FTS5Pattern where T: TableRecord, T: ColumnExpressible {
return try makeFTS5Pattern(rawPattern: rawPattern, forTable: table.databaseTableName)
}
/// This is a custom implementation of the `afterNextTransaction` method which executes the closures within their own
/// transactions to allow for nesting of 'afterNextTransaction' actions
///
/// **Note:** GRDB doesn't notify read-only transactions to transaction observers
func afterNextTransactionNested(
using dependencies: Dependencies,
onCommit: @escaping (Database) -> Void,
onRollback: @escaping (Database) -> Void = { _ in }
) {
dependencies.mutate(cache: .transactionObserver) {
$0.add(self, dedupeId: UUID().uuidString, onCommit: onCommit, onRollback: onRollback)
}
}
func afterNextTransactionNestedOnce(
dedupeId: String,
using dependencies: Dependencies,
onCommit: @escaping (Database) -> Void,
onRollback: @escaping (Database) -> Void = { _ in }
) {
dependencies.mutate(cache: .transactionObserver) {
$0.add(self, dedupeId: dedupeId, onCommit: onCommit, onRollback: onRollback)
}
}
}
internal class TransactionHandler: TransactionObserver {
private let dependencies: Dependencies
private let identifier: String
private let onCommit: (Database) -> Void
private let onRollback: (Database) -> Void
init(
identifier: String,
onCommit: @escaping (Database) -> Void,
onRollback: @escaping (Database) -> Void,
using dependencies: Dependencies
) {
self.dependencies = dependencies
self.identifier = identifier
self.onCommit = onCommit
self.onRollback = onRollback
}
// Ignore changes
func observes(eventsOfKind eventKind: DatabaseEventKind) -> Bool { false }
func databaseDidChange(with event: DatabaseEvent) { }
func databaseDidCommit(_ db: Database) {
dependencies.mutate(cache: .transactionObserver) { $0.remove(for: identifier) }
do {
try db.inTransaction {
onCommit(db)
return .commit
}
}
catch {
Log.warn(.storage, "afterNextTransactionNested onCommit failed")
}
}
func databaseDidRollback(_ db: Database) {
dependencies.mutate(cache: .transactionObserver) { $0.remove(for: identifier) }
onRollback(db)
}
}
// MARK: - TransactionObserver Cache
internal extension Storage {
class TransactionObserverCache: TransactionObserverCacheType {
private let dependencies: Dependencies
public var registeredHandlers: [String: TransactionHandler] = [:]
// MARK: - Initialization
public init(using dependencies: Dependencies) {
self.dependencies = dependencies
}
// MARK: - Functions
public func add(
_ db: Database,
dedupeId: String,
onCommit: @escaping (Database) -> Void,
onRollback: @escaping (Database) -> Void
) {
// Only allow a single observer per `dedupeId` per transaction, this allows us to
// schedule an action to run at most once per transaction (eg. auto-scheduling a ConfigSyncJob
// when receiving messages)
guard registeredHandlers[dedupeId] == nil else { return }
let observer: TransactionHandler = TransactionHandler(
identifier: dedupeId,
onCommit: onCommit,
onRollback: onRollback,
using: dependencies
)
db.add(transactionObserver: observer, extent: .nextTransaction)
registeredHandlers[dedupeId] = observer
}
public func remove(for identifier: String) {
registeredHandlers.removeValue(forKey: identifier)
}
}
}
// MARK: - TransactionObserverCacheType
/// This is a read-only version of the Cache designed to avoid unintentionally mutating the instance in a non-thread-safe way
internal protocol TransactionObserverImmutableCacheType: ImmutableCacheType {
var registeredHandlers: [String: TransactionHandler] { get }
}
internal protocol TransactionObserverCacheType: TransactionObserverImmutableCacheType, MutableCacheType {
var registeredHandlers: [String: TransactionHandler] { get }
func add(
_ db: Database,
dedupeId: String,
onCommit: @escaping (Database) -> Void,
onRollback: @escaping (Database) -> Void
)
func remove(for identifier: String)
}