Combine: How to clean up resources while an AnyCancellable is being cancelled? - swift

Overview:
I have a async task to fetch from the database
I have created a Future for the async task (fetching from the database).
Question:
How can execute custom code when the Future is cancelled?
Purpose:
I would like the database connection to be closed when the subscription is cancelled.
For example, I would like to use Combine to rewrite this helper method:
// Similar to https://developer.apple.com/documentation/coredata/nspersistentcontainer/1640564-performbackgroundtask
func withDatabaseFTSContext(block: #escaping (FMDatabase?) -> Void) {
queue.async {
guard let database = self.database else {
block(nil)
return
}
database.open()
let simpleTokenizer = FMSimpleTokenizer(locale: nil)
FMDatabase.registerTokenizer(simpleTokenizer, withKey: "simple")
database.installTokenizerModule()
block(database)
database.close()
}
}
Could I leverage Combine to rewrite this method to return FMDatabase as a parameter of a publisher?
I was attempting to use Combine but it does not work. The database will be closed before cancel()
private func withDatabaseFTSContext() -> AnyPublisher<FMDatabase?, Never> {
return Future<FMDatabase?, Never> { promise in
self.queue.async {
guard let database = self.database else {
promise(.success(nil))
return
}
database.open()
let simpleTokenizer = FMSimpleTokenizer(locale: nil)
FMDatabase.registerTokenizer(simpleTokenizer, withKey: "simple")
database.installTokenizerModule()
promise(.success(database))
database.close() // When to close this database? Currently it will be closed before `cancel()`
}
}.eraseToAnyPublisher()
}

Short answer: there isn't a callback that triggers through to the underlying Future that you can use to clean things up on a subscriber cancel. In the Combine design, these functions are very intentionally separated and don't have reference links back to their publishers.
(In addition, Future is a tricky figure in the Combine world because the closure is invoked immediately upon creation time, rather than when you have a subscription (if you want that, wrap in the Future publisher in a Deferred publisher)).
All that being said, what you likely want to do to solve your underlying problem is reframe how you're treating this to separate the concerns of managing the FMDB instance and publishing data. One pattern that's been reasonably useful in this context is to the make an object that holds the lifetime of the FMDB reference, and handle cleaning up resources on it's deinit(). You can then also have a function which vends a Publisher of whatever you need from that same object, and then the cancellation of the request is changed semantically to only cancelling getting the database, not cancelling and cleaning up the database connection.

Related

`NSDocument`'s `data(ofType:)` getting data from (async) `actor`

I've a document based macOS, that's using a NSDocument based subclass.
For writing the document's file I need to implement data(ofType:) -> Data which should return the document's data to be stored on disk. This is (of course) a synchronous function.
My data model is an actor with a function that returns a Data representation.
The problem is now that I need to await this function, but data(ofType:) wants the data synchronously.
How can I force-wait (block the main thread) until the actor has done its work and get the data?
EDIT:
In light of Sweepers remark that this might be an XY-problem I tried making the model a #MainActor, so the document can access the properties directly. This however doesn't allow me to create the model in the first place:
#MainActor class Model {}
class Document: NSDocument {
let model = Model() <- 'Call to main actor-isolated initializer 'init()' in a synchronous nonisolated context'
}
I then tried to make the whole Document a #MainActor, but that makes my whole app to collapse in compiler errors. Even the simplest of calls need to be performed async. This doesn't allow any kind of upgrade path to the new concurrency system.
In the past my model was protected by a serial background queue and I could basically do queue.sync {} to get the needed data out safely (temporarily blocking the main queue).
I've looked into the saveToURL:ofType:forSaveOperation:completionHandler: and I think I can use this very much to my need. It allows async messaging that saving is finished, so I now override this method and in an async Task fetch the data from the model and store it in temporarily. I then call super, which finally calls data(forType:) where I return the data.
Based on the idea by #Willeke in the comments, I came up with the following solution:
private var snapshot: Model.Snapshot?
override func save(to url: URL, ofType typeName: String, for saveOperation: NSDocument.SaveOperationType, completionHandler: #escaping (Error?) -> Void) {
//Get the data and continue later
Task {
snapshot = await model.getSnapshot()
super.save(to: url, ofType: typeName, for: saveOperation, completionHandler: completionHandler)
}
}
override func data(ofType typeName: String) throws -> Data {
defer { snapshot = nil }
guard let snapshot = snapshot else {
throw SomeError()
}
let encoder = JSONEncoder()
let data = try encoder.encode(snapshot)
return data
}
As the save() function is prepared to handle the save result asynchronous we first take the snapshot of the data and then let the save function continue.

In a Combine Publisher chain, how to keep inner objects alive until cancel or complete?

I've created a Combine publisher chain that looks something like this:
let pub = getSomeAsyncData()
.mapError { ... }
.map { ... }
...
.flatMap { data in
let wsi = WebSocketInteraction(data, ...)
return wsi.subject
}
.share().eraseToAnyPublisher()
It's a flow of different possible network requests and data transformations. The calling code wants to subscribe to pub to find out when the whole asynchronous process has succeeded or failed.
I'm confused about the design of the flatMap step with the WebSocketInteraction. That's a helper class that I wrote. I don't think its internal details are important, but its purpose is to provide its subject property (a PassthroughSubject) as the next Publisher in the chain. Internally the WebSocketInteraction uses URLSessionWebSocketTask, talks to a server, and publishes to the subject. I like flatMap, but how do you keep this piece alive for the lifetime of the Publisher chain?
If I store it in the outer object (no problem), then I need to clean it up. I could do that when the subject completes, but if the caller cancels the entire publisher chain then I won't receive a completion event. Do I need to use Publisher.handleEvents and listen for cancellation as well? This seems a bit ugly. But maybe there is no other way...
.flatMap { data in
let wsi = WebSocketInteraction(data, ...)
self.currentWsi = wsi // store in containing object to keep it alive.
wsi.subject.sink(receiveCompletion: { self.currentWsi = nil })
wsi.subject.handleEvents(receiveCancel: {
wsi.closeWebSocket()
self.currentWsi = nil
})
Anyone have any good "design patterns" here?
One design I've considered is making my own Publisher. For example, instead of having WebSocketInteraction vend a PassthroughSubject, it could conform to Publisher. I may end up going this way, but making a custom Combine Publisher is more work, and the documentation steers people toward using a subject instead. To make a custom Publisher you have to implement some of things that the PassthroughSubject does for you, like respond to demand and cancellation, and keep state to ensure you complete at most once and don't send events after that.
[Edit: to clarify that WebSocketInteraction is my own class.]
It's not exactly clear what problems you are facing with keeping an inner object alive. The object should be alive so long as something has a strong reference to it.
It's either an external object that will start some async process, or an internal closure that keeps a strong reference to self via self.subject.send(...).
class WebSocketInteraction {
private let subject = PassthroughSubject<String, Error>()
private var isCancelled: Bool = false
init() {
// start some async work
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
if !isCancelled { self.subject.send("Done") } // <-- ref
}
}
// return a publisher that can cancel the operation when
var pub: AnyPublisher<String, Error> {
subject
.handleEvents(receiveCancel: {
print("cancel handler")
self.isCancelled = true // <-- ref
})
.eraseToAnyPublisher()
}
}
You should be able to use it as you wanted with flatMap, since the pub property returned publisher, and the inner closure hold a reference to self
let pub = getSomeAsyncData()
...
.flatMap { data in
let wsi = WebSocketInteraction(data, ...)
return wsi.pub
}

What is the right approach for resolving save operations with Swift Combine

I have a classic situation where I want to commit some action only once. For example update some entity in database using Swift Combine. My problem is that I don't really know what is the best approach for doing something only once. How do I unsubscribe when the update is finished?
This is code snippet through the layers that I am currently using:
ViewModel:
let settingsModel: LocalSettingsModel
func saveLocalSettings(){
let cancelable = settingsUseCase
.saveLocalSettings(localSettingsModel: settingsModel)
.sink(receiveCompletion: {_ in
print("Completed!!!")
}) { _ in
print("Result of Save operation!!!")
}
}
UseCase:
func saveLocalSettings(settings: LocalSettingsModel) -> AnyPublisher<LocalSettingsModel, Error> {
return repository.saveLocalSettings(settings: settings)
}
Repository:
guard let realmSettings = LocalSettingsRealmModel(fromModel: settings) else {
return Fail<LocalSettingsModel, Error>(error: .postconditionError(errorMessage: ""))
.eraseToAnyPublisher()
}
return self.localDataSource
.saveLocalSettings(localSettings: realmSettings)
.receive(on: DispatchQueue.main)
.subscribe(on: DispatchQueue.global())
.mapError { (error) -> Error in
// do error mapping
}
.compactMap { settings in
return (LocalSettingsModel(fromModel: settings))
}
.eraseToAnyPublisher()
Data Source:
func saveLocalSettings(localSettings: LocalSettingsRealmModel) -> AnyPublisher<LocalSettingsRealmModel, LocalDataSourceError> {
do {
return Just(try saveSettings(localSettings: localSettings))
.mapError({ (Never) -> LocalDataSourceError in})
.eraseToAnyPublisher()
} catch let error as NSError {
// return some error
}
}
func saveSettings(localSettings: LocalSettingsRealmModel) throws -> LocalSettingsRealmModel
{
let realm = try Realm()
try realm.write {
realm.add(localSettings, update: .modified)
}
return localSettings
}
I would really appreciate some pointers in the direction of what is a good practice when we are not expecting continuous stream of information in reactive world like in case of functions whose purpose is to execute single action. Do I really need to use Just() like in this example or is there a way do deal with this kind of situations from subscriber side.
You want something that will convert a Publisher into a single value and then terminate, and the sequence operators in Combine are what you want to use for that kind of thing.
Combine is set up to deal with one OR many values. So you, as the consumer, need to give it some way to constrain the potentially many values into a single value, if you want to use an assign subscriber to set a value (or sink subscriber to invoke a closure where you do your save, in your case).
The sequence operators in Combine is where I'd think to look, but I can't really describe which one without knowing how many values and how you'd choose which one to apply. The two "easy" options are either first or last, but there's a variety of sequence operators that let you construct more complicated choices (including firstWhere and lastWhere which let you determine based on your own closure, which can be darned handy.
The embedded links are all to the online/free-version of Using Combine (disclosure: which I wrote) - and while I don't have any explicit examples about the sequence operators, I did flesh out the reference details for them in the book quite a bit.
Unless you're explicitly working from a publisher, you may find it easier to use a Promise library - depending on what's triggering your save. I don't know the Realm end of this to know what their API focuses on, and if you've made the publisher that's generating the data, or if that's coming from their API - and hence your desire to using Combine to solve this.

How to call every struct method inside write transaction

I created struct Repository for manipulating with objects of Realm database (changing some properties, adding new objects, deleting, etc.). When I want to write to the database, I have to do it inside do-try-catch block, so I created a method with completion which I call every time I need to write something to the database
private func action(_ completion: () -> Void) {
do {
try realm.write {
completion()
}
} catch {
print(error)
}
}
then I call methods for manipulating with objects like this:
func createObject(_ object: MyObject) {
action {
realm.add(object)
}
}
func deleteObject(_ object: MyObject) {
action {
realm.delete(object)
}
}
func setTitleForObject(_ object: MyObject, title: String) {
action {
object.title = title
}
}
...
My question is, is there any way how I can call every method inside this Repository struct inside write transaction in do-try-catch block by default instead of calling it inside completion of action? (or is some better way how to write to the Realm database without do-try-catch block?)
Short answer is no, there is no way to write data to realm without write transaction and without try-catch.
realm.write() is a convenient wrapper of transaction building with beginWrite() and commitWrite() calls.
These two functions build a transaction and commitWrite() is throwable, so you need to wrap to try-catch, anyway.
See https://realm.io/docs/swift/latest#writes
Example of using beginWrite()+commitWrite() https://realm.io/docs/swift/latest#interface-driven-writes
There are a lot of failures could happen during write transactions. So, simply, it is not safe to not to handle it somehow.
Also grouping write transactions by "action" is not a good idea if you going to process big amounts of objects because write transactions are costly. You'd rather group these changes to a single transaction instead of having a lot of small transactions.

Realm notifications registration while in write transaction

I understand that you can not register a Realm .observe block on an object or collection if the Realm is in a write transaction.
This is easier to manage if everything is happening on the main thread however I run into this exception often because I prefer to hand my JSON parsing off to a background thread. This works great because I don't have to bog down the main thread and with Realm's beautiful notification system I can get notified of all modifications if I have already registered to listen for those changes.
Right now, if I am about to add an observation block I check to make sure my Realm is not in a write transaction like this:
guard let realm = try? Realm(), !realm.isInWriteTransaction else {
return
}
self.myToken = myRealmObject.observe({ [weak self] (change) in
//Do what ever
}
This successfully guards against this exception. However I never get a chance to re - register this token unless I get a little creative.
Does the Realm team have any code examples/ suggestions on a better pattern to avoid this exception? Any tricks I'm missing to successfully register the token?
In addition to the standard function, I do use an extension for Results to avoid this in general. This issue popped up, when our data load grew bigger and bigger.
While we do now rewrite our observe functions logic, this extension is an interims solution to avoid the crashes at a first place.
Idea is simple: when currently in a write transaction, try it again.
import Foundation
import RealmSwift
extension Results {
public func safeObserve(on queue: DispatchQueue? = nil,
_ block: #escaping (RealmSwift.RealmCollectionChange<RealmSwift.Results<Element>>) -> Void)
-> RealmSwift.NotificationToken {
// If in Write transaction, call it again
if self.realm?.isInWriteTransaction ?? false {
DispatchQueue.global().sync {
Thread.sleep(forTimeInterval: 0.1) // Better to have some delay than a crash, hm?
}
return safeObserve(on: queue, block)
}
// Aight, we can proceed to call Realms Observe function
else {
return self.observe(on: queue, block)
}
}
}
Then call it like
realmResult.safeObserve({ [weak self] (_: RealmCollectionChange<Results<AbaPOI>>) in
// Do anything
})