NSPersistentStoreRemoteChange

Published by malhal on

I wondered why NSPersistentStoreRemoteChange doesn’t contain any info about what process made the change. For example it isn’t possible to know if the change even was made by the current process or not. This would be useful so when you make a save you know if you can skip reloading any data visible on screen. Before I worked around the problem I decided to find out how it is implemented.

As of iOS 17 _NSSQLCoreConnectionObserver is part of the story. Funny in earlier versions of iOS, e.g. 14b2 this class name had a typo _NSSQLCoreConnectionObsever.

initWithSQLCore calls [NSSQLCore notifyPostName] which in turn calls [_PFRoutines _remoteChangeNotificationNameForStore:] to format a string like “com.apple.coredata.NSPersistentStoreRemoteChangeNotification.remotenotification.C2377B1D-439A-407D-A385-266450F4D261” where the UUID is the store ID.

It then uses dispatch_queue_create with @"%@.queue.%p" to create a queue named:

com.apple.coredata.NSPersistentStoreRemoteChangeNotification.remotenotification.C2377B1D-439A-407D-A385-266450F4D261.queue.0x28 I’m not sure what the %p is, maybe a version number?

It then uses notify_register_dispatch to listen for messages posted to this queue. It uses notify_get_state to get an Int from the message. It then calls -[_NSSQLCoreConnectionObsever _postRemoteChangeNotificationWithTransactionID:] which first dispatches to the global queue and then calls [persistentStoreCoordinator recordRemoteQueryGenerationDidChange] and then _postStoreRemoteChangeNotificationsForStore to built the userInfo and post the application user notification using the global dispatch queue.

In [NSSQLCore _postChangeNotificationWithTransactionID: int] it uses notify_set_state to set an Int in some shared file which uses a token to identity it. The Int that is saved is either 0 or the transaction ID if persistent history is turned on. The info in the docs shows this is some really primitive XPC, not what we are used to with sending a whole NSDictionary between processes. The notification is posted with uint32_t notify_post(const char *name);

It seems to me it could be possible for a client to figure out if it sent the message or it came from someone else but it perhaps is slightly fragile. Each client could choose its own random Int and store it in the notify_set_state. Or perhaps we could choose our own Int, say as a property on the context, e.g. “clientID” similar to transactionAuthor. Personally I am very wary of enabling persistent history tracking since it seems to me there is no clear way of when it should be deleted and as far as I know when it is used by NSPersistentCloudKitContainer it is just never deleted at all which seems very irresponsible programming.

As for a workaround, there are 2 directions one could go in. Make a store per client process that has changes made instead of actual records and then both clients read the other’s change log to build its own store of records that are the ones presented in the UI. Or keep the shared record store and have client only stores that just have info about the latest save they made. Using Core Data for this is probably overkill and a simple file with NSFilePresenter would suffice and probably be a lot more reliable that these primitive store changed dispatch notifications.

Categories: CoreData