How to Be Notified if the Owner Removes Me from a CKShare on CloudKit - cloudkit

Let's say the owner of a record shares it with me. I get sent a share link and I open it and accept the share like this:
let operation = CKAcceptSharesOperation(shareMetadatas: [metadata])
operation.acceptSharesCompletionBlock = { error in
if let error = error{
print("accept share error: \(error)")
}else{
//Share accepted...
}
}
CloudKit.container.add(operation)
I am also previously subscribed to the Shared database already like so:
let subscriptionSharedDatabase = CKDatabaseSubscription(subscriptionID: "subscriptionSharedDatabase")
let sharedInfo = CKSubscription.NotificationInfo()
sharedInfo.shouldSendContentAvailable = true
sharedInfo.alertBody = "" //This needs to be set or pushes don't get sent
subscriptionSharedDatabase.notificationInfo = sharedInfo
let subShared = CKModifySubscriptionsOperation(subscriptionsToSave: [subscriptionSharedDatabase], subscriptionIDsToDelete: nil)
CloudKit.sharedDB.add(subShared)
But now let's say the owner of the CKShare removes me as a participant on that record and saves the updated participants list to CloudKit.
As far as I can tell, the only notification I get is another shared database subscription (subscriptionSharedDatabase) change, but no records are changed or deleted (I looked and there are no changed records when I fetch them).
As far as I know, the only way to be notified of changes to the participants on a CKShare is to subscribe to notifications on the cloudkit.share record type, but that isn't available to me in the shared database, right?
How can I be notified when I am removed from a CKShare?

Interesting how this has no answers. I just implemented some CKShare-related code and it seems to work fairly predictably. Here is the basic approach.
1) at the start of my app, I do CKFetchRecordZonesOperation.fetchAllRecordZonesOperation() on the shared database, to get all the current record zones that are shared with me.
2) I set up CKDatabaseSubscription on the shared database as you suggest.
3) Upon receiving this notification on the shared database, I do a batch CKFetchRecordZoneChangesOperation across all the shared record zones. (You can pass it multiple record zones, together with a server change token for each zone, to do a bulk updates query.)
4) If any shares were unshared but the record zones themselves are still valid, I observe that the recordWithIDWasDeletedBlock is run twice, once with the unshared CKRecord and once with the related CKShare.
5) (I didn’t yet fully figure out this part) if the share was the last one for a given user, the whole shared record zone from that user gets removed from my shared database. I didn’t yet fully figure out how to best handle this part. I could query for fresh record zones every time I get a CKDatabaseNotification, but that seems wasteful. I see that I can run CKFetchDatabaseChanges operation that informs me of changed zones, but I have yet to figure out when is the best time to run it.

Related

Swift and Cloud Firestore Transactions - getDocuments?

Transactions in Cloud Firestore support getting a document using transaction.getDocument, but even though there is a .getDocuments method, there doesn’t seem to be a .getDocuments for getting multiple documents that works with a transaction.
I have a Yelp-like app using a Cloud Firestore database with the following structure:
- Places to rate are called spots.
- Each spot has a document in the spots collection (identified by a unique documentID).
- Each spot can have a reviews collection containing all reviews for that spot.
- Each review is identified by its own unique documentID, and each review document contains a rating of the spot.
Below is an image of my Cloud Firestore setup with some data.
I’ve tried to create a transaction getting data for all of the reviews in a spot, with the hope that I could then make an updated calculation of average review & save this back out to a property of the spot document. I've tried using:
let db = Firestore.firestore()
db.runTransaction({ (transaction, errorPointer) -> Any? in
let ref = db.collection("spots").document(self.documentID).collection("reviews")
guard let document = try? transaction.getDocuments(ref) else {
print("*** ERROR trying to get document for ref = \(ref)")
return nil
}
…
Xcode states:
Value of type ‘Transaction’ has no member ‘getDocuments’.
There is a getDocument, which that one can use to get a single document (see https://firebase.google.com/docs/firestore/manage-data/transactions).
Is it possible to get a collection of documents in a transaction? I wanted to do this because each place I'm rating (spot) has an averageRating, and whenever there's a change to one of the ratings, I want to call a function that:
- starts a transaction (done)
- reads in all of the current reviews for that spot (can't get to work)
- calculates the new averageRating
- updates the spot with the new averageRating value.
I know Google's FriendlyEats uses a technique where each change is applied to the current average rating value, but I'd prefer to make a precise re-calculation with each change to keep numerical precision (even if it's a bit more expensive w/an additional query).
Thanks for advice.
No. Client libraries do not allow you to make queries inside of transactions. You can only request specific documents inside of a query. You could do something really hacky, like run the query outside of the transaction, then request every individual document inside the transaction, but I would not recommend that.
What might be better is to run this on the server side. Like, say, with a Cloud Function, which does allow you to run queries inside transactions. More importantly, you no longer have to trust the client to update the average review score for a restaurant, which is a Bad Thing.
That said, I still might recommend using a Cloud Function that does some of the same logic that Friendly Eats does, where you say something along the lines of New average = Old average + new review / (Total number of reviews) It'll make sure you're not performing excessive reads if your app gets really popular.

ObjectMapper + Realm + Alamofire

I map my objects with ObjectMapper, that are delivered by Alamofire and persist them in Realm.
Everthing is working fine. But how can I delete objects, that exist in Realm but have been deleted in my webservice?
Update: Based on the answer below I currently ended with this code:
if let overviewItemsArray = response.result.value{
do{
try self.realm.write{
self.realm.delete(self.realm.objects(OverviewItem))
self.realm.add(overviewItemsArray, update: true)
}
}
catch let err as NSError {
logger.error("Error with realm: \(err.localizedDescription)")
}
overviewItemsAsList.removeAll()
overviewItemsAsList.appendContentsOf(self.realm.objects(OverviewItem)
.sorted("sortOrder", ascending: true))
successHandler(overviewItemsAsList)
}
Perhaps somebody has further input how to improve this. I have 10 objects of this type. But on other objects I get 1500 items.
I finally figured it out and it works very well. I compute the difference of the cached data and the new data and delete it:
private func deleteOrphans(existingData: List<VotingHeader>, fetchedData:[VotingHeader]){
guard existingData.count>0 else {
return
}
let existingIDs = Set(existingData.map({$0.votingID}))
let incomingIDs = fetchedData.map({$0.votingID})
let idsToDelete = existingIDs.subtract(incomingIDs)
if idsToDelete.count>0{
let itemsToDelete = self.realm.objects(VotingHeader).filter("votingID IN %#",idsToDelete)
try! self.realm.write{
self.realm.delete(itemsToDelete)
}
}
}
You could:
-use updated_at field and update that field via id, after that delete all objects that haven't been updated or
-delete all objects and then insert new ones (expensive, I wouldn't do such a thing)
I think that you should go with the first option especially if you have a lot of data, when you get your response from web service, go through each and update corresponding records in database with NSDate() and afterwards, delete objects that are "old". Note that this is not updated_at from server but your local storage.
The other one is fine if you have small amount of data and you are sure that it will be fast and cheap.
This sounds like you need to implement some sort of mechanism for determining which objects were deleted on the server, and explicitly relaying that information to your local devices to reflect that change. I can think of a few possible solutions:
If you have control of the API, you could implement a system where your device tells the server the timestamp of when it last called the API, and the server could then return a list of the ID numbers in its response for the entries that had been deleted after that time.
If you're simply downloading the same list every time, rather than blow away everything in the Realm file each time, perform a manual loop through each entry in the Realm file and see if it has a corresponding entry in the downloaded list. If not, delete it. Granted, this will still be a relatively slow operation, but it would be much faster than deleting/recreating everything each time.
Since it sounds like Realm is being used to cache server information here, you could just set a really strict expiry time on the Realm objects that would delete them if it is exceeded, or extended if the object is refreshed from the server.
I hope that helps!

CKFetchRecordsOperation never returns any keys

I have a single record in my public (development) database, with several populated fields. When I use a CKFetchRecordsOperation to fetch that record, the fetch succeeds, but the resulting CKRecord always contains no keys. This happens whether I explicitly set desiredKeys on my fetch operation, or leave it as nil. The record and its keys have been unchanged for a long time, so it's not a propagation delay.
There's only one record in the public database and its ID matches what I get, so I'm sure the correct record is being fetched; it just never has any keys. For example, the below code always prints keys: [].
let op = CKFetchRecordsOperation(recordIDs:[...])
op.desiredKeys = [...] // commenting this has no effect
op.fetchRecordsCompletionBlock = {records, error in
if let e = error {
print("error:", e)}
for (id, r) in records ?? [:] {
print("keys:", r.allKeys())}}
db.addOperation(op)
I've also tried accessing the individual keys via subscript notation, but they're always nil. I'm at a loss at this point. There are no errors, and the record I want is being found, so why are there never any keys?
Update: I've noticed that the CloudKit Dashboard always says "Reindexing development" in the bottom left corner, even days after my last change. Furthermore, I tried creating a second record (via the Dashboard) and my fetch fails to even find it, after leaving plenty of time for propagation.
Following a link from this SO question to an Apple developer forum thread about this problem, I found one post suggesting it happens only to records with Asset fields. I will try experimenting when I have time, though I can't avoid using Assets in this case so it might not be much help.
Update 2: I tried resetting my development environment and then recreating the same records as before. On the plus side, the "Reindexing" message in the CloudKit Dashboard is finally gone. Unfortunately, my code now fails to find any of my records, even though the code remains unchanged and I've triple-checked everything.
I also tried creating a record type with no Asset fields (see first update above), but it didn't seem to change anything.
Turns out I was querying the private database, when the records I wanted were in the public one.

NSMutableURLRequest on succession of another NSMutableURLRequest's success

Basically, I want to implement SYNC functionality; where, if internet connection is not available, data gets stored on local sqlite database. Whenever, internet connection is available, SYNC gets into the action.
Now, Say for example; 5 records are stored locally, and then internet connection is available. I want the server to be updated. So, What I do currently is:
Post first record to the server.
Wait for the success of first request.
Post local NSNotification to routine, that the first record has been updated on server & now second request can go.
The routine fires the second post request on server and so on...
Question: Is this approach right and efficient enough to implement SYNC functionality; OR anything I should change into it ??
NOTE: Records to be SYNC will have no limit in numbers.
Well it depends on the requirements on the data that you save. If it is just for backup then you should be fine.
If the 5 records are somehow dependent on each other and you need to access this data from another device/application you should take care on the server side that either all 5 records are written or none. Otherwise you will have an inconsistent state if only 3 get written.
If other users are also reading / writing those data concurrently on the server then you need to implement some kind of lock on all records before writing and also decide how to handle conflicts when someone attempts to overwrite somebody else changes.

How to guard against repeated request?

we have a button in a web game for the users to collect reward. That should only be clicked once, and upon receiving the request, we'll mark it collected in DB.
we've already blocked the buttons in the client from repeated clicking. But that won't help if people resend the package multiple times to our server in short period of time.
what I want is a method to block this from server side.
we're using Playframework 2 (2.0.3-RC2) for server side and so far it's stateless, I'm tempted to use a Set to guard like this:
if processingSet has userId then BadRequest
else put userId in processingSet and handle request
after that remove userId from that Set
but then I'd have to face problem like Updating Scala collections thread-safely and still fail to block the user once we have more than one server behind load balancing.
one possibility I'm thinking about is to have a table in DB in place of the processingSet above, but that would incur 1+ DB operation per request, are there any better solution~?
thanks~
Additional DB operation is relatively 'cheap' solution in that case. You should use it if you'e planning to save the buttons state permanently.
If the button is disabled only for some period of time (for an example until the game is over) you can also consider using the cache API however keep in mind that's not dedicated for solutions which should be stored for long time (it should not be considered as DB alternative).
Given that you're using Mongo and so don't have transactions spanning separate collections, I think you can probably implement this guard using an atomic operation - namely "Update if current", which is effectively CompareAndSwap.
Assuming you've got a collection like "rewards" which has a "collected" attribute, you can update the collected flag to true only if it is currently false and if that operation doesn't fail you can proceed to apply the reward knowing that for any other requests the same operation will fail.