swift: Improve HLAPI invite handler experience
This CL implements vanadium/issues#1408 for Swift, which hides
unnecessary invites from already joined syncgroups, including
userdata. It also auto-joins syncgroups with the user's blessings.
Change-Id: Iac59cc1683aed636a3506c1f66dc1d41efefaa06
diff --git a/Syncbase/Source/Database.swift b/Syncbase/Source/Database.swift
index 0f79ac9..3e28791 100644
--- a/Syncbase/Source/Database.swift
+++ b/Syncbase/Source/Database.swift
@@ -86,10 +86,14 @@
// the original reference to Database). Instead, we store the databaseId to make sure that any two
// Database instances generated from Syncbase.database behave the same with respect to
// adding/removing watch handlers.
- private static let syncgroupInviteHandlersMu = NSLock()
- private static let watchChangeHandlersMu = NSLock()
- private static var syncgroupInviteHandlers: [SyncgroupInviteHandler: SyncbaseCore.SyncgroupInvitesScanHandler] = [:]
- private static var watchChangeHandlers: [WatchChangeHandler: WatchOperation] = [:]
+ static var watchChangeHandlers: [WatchChangeHandler: WatchOperation] = [:]
+ static let watchChangeHandlersMu = NSLock()
+ static var syncgroupInviteHandlers: [SyncgroupInviteHandler: SyncbaseCore.SyncgroupInvitesScanHandler] = [:]
+ static let syncgroupInviteHandlersMu = NSLock()
+ // These are the same as above except reserved for internal use only. If we used the dictionaries
+ // above then the removeAll* functions would remove the internal handlers.
+ static var internalWatchChangeHandlers: [WatchChangeHandler: WatchOperation] = [:]
+ static let internalWatchChangeHandlersMu = NSLock()
func createIfMissing() throws {
do {
@@ -210,18 +214,34 @@
/// Notifies `handler` of any existing syncgroup invites, and of all subsequent new invites.
public func addSyncgroupInviteHandler(handler: SyncgroupInviteHandler) {
- Database.syncgroupInviteHandlersMu.lock()
- defer { Database.syncgroupInviteHandlersMu.unlock() }
- let coreHandler = SyncbaseCore.SyncgroupInvitesScanHandler(onInvite: { invite in
- handler.onInvite(SyncgroupInvite(
- syncgroupId: Identifier(coreId: invite.syncgroupId),
- inviterBlessingNames: invite.blessingNames))
+ let coreHandler = SyncbaseCore.SyncgroupInvitesScanHandler(onInvite: { coreInvite in
+ // We don't automatically pass the invite to the end-user, see
+ // https://github.com/vanadium/issues/issues/1408 for more details.
+ if coreInvite.syncgroupId.name == Syncbase.UserdataSyncgroupName {
+ // Ignore userdata syncgroup which is auto-joined if the blessings are correct in Go.
+ return
+ }
+ let invite = SyncgroupInvite(coreInvite: coreInvite)
+ if invite.syncgroupId.blessing == (try? Principal.userBlessing()) {
+ // Ignore syncgroups with our own blessing -- they are auto-joined by the internal
+ // handler started post-login in Syncbase.swift.
+ return
+ }
+ if (try? Syncbase.syncgroupInUserdata(invite.syncgroupId)) ?? false {
+ // Ignore syncgroups that have already been joined.
+ return
+ }
+
+ handler.onInvite(invite)
})
+
do {
try coreDatabase.scanForSyncgroupInvites(
try databaseId.encode(),
handler: coreHandler)
+ Database.syncgroupInviteHandlersMu.lock()
Database.syncgroupInviteHandlers[handler] = coreHandler
+ Database.syncgroupInviteHandlersMu.unlock()
} catch {
handler.onError(error)
}
@@ -465,14 +485,12 @@
defer { Database.watchChangeHandlersMu.unlock() }
if let op = Database.watchChangeHandlers[handler] {
op.cancel()
+ Database.watchChangeHandlers[handler] = nil
}
- Database.watchChangeHandlers[handler] = nil
}
/// Makes it so all watch change handlers stop receiving notifications attached to this database.
public func removeAllWatchChangeHandlers() {
- // Grab a local copy by value so we don't need to worry about concurrency or deadlocking
- // on the main mutex.
Database.watchChangeHandlersMu.lock()
defer { Database.watchChangeHandlersMu.unlock() }
let handlers = Database.watchChangeHandlers
@@ -482,6 +500,16 @@
Database.watchChangeHandlers.removeAll()
}
+ func removeAllInternalWatchChangeHandlers() {
+ Database.internalWatchChangeHandlersMu.lock()
+ defer { Database.internalWatchChangeHandlersMu.unlock() }
+ let handlers = Database.internalWatchChangeHandlers
+ for op in handlers.values {
+ op.cancel()
+ }
+ Database.internalWatchChangeHandlers.removeAll()
+ }
+
public var description: String {
return "[Syncbase.Database id=\(databaseId)]"
}
diff --git a/Syncbase/Source/Syncbase.swift b/Syncbase/Source/Syncbase.swift
index 3ebf4a4..1dd325f 100644
--- a/Syncbase/Source/Syncbase.swift
+++ b/Syncbase/Source/Syncbase.swift
@@ -21,6 +21,9 @@
static var db: Database?
// The userdata collection, created post-login.
static var userdataCollection: Collection?
+ // The handler for auto-joining invites that match our user's blessings. We keep it in a var
+ // in order to remove it on `shutdown`.
+ static var autojoinInviteHandler: SyncbaseCore.SyncgroupInvitesScanHandler?
// Options for opening a database.
static var cloudName: String?
static var cloudBlessing: String?
@@ -144,6 +147,15 @@
/// Shuts down the Syncbase service. You must call configure again before any calls will work.
public static func shutdown() {
+ // We don't need to log/worry about the catch {} -- the exception is if we can't get a database
+ // handle, which would happen if we haven't logged in, etc. The actual remove handler does not
+ // throw any errors.
+ do { try database().removeAllSyncgroupInviteHandlers() } catch { }
+ do { try database().removeAllWatchChangeHandlers() } catch { }
+ do { try database().removeAllInternalWatchChangeHandlers() } catch { }
+ if let handler = autojoinInviteHandler {
+ do { try database().coreDatabase.stopSyncgroupInvitesScan(handler) } catch { }
+ }
Syncbase.didStartShutdown = true
SyncbaseCore.Syncbase.shutdown()
Syncbase.didInit = false
@@ -180,12 +192,39 @@
NSLog("Syncbase - Error watching userdata syncgroups: %@", "\(err)")
}
}))
+ // Auto-join syncgroups with the same blessings.
+ autojoinInviteHandler = SyncbaseCore.SyncgroupInvitesScanHandler(onInvite: onSyncgroupInvite)
+ try coreDb.scanForSyncgroupInvites(
+ try database.databaseId.encode(),
+ handler: autojoinInviteHandler!)
}
Syncbase.db = database
Syncbase.didPostLogin = true
}
+ private static func onSyncgroupInvite(invite: SyncbaseCore.SyncgroupInvite) {
+ // Auto-accept groups that have blessings that match our own.
+ let syncgroupId = Identifier(coreId: invite.syncgroupId)
+ guard invite.syncgroupId.blessing == (try? Principal.userBlessing()) &&
+ // Ignore userdata syncgroup which is auto-joined if the blessings are correct in Go.
+ invite.syncgroupId.name != Syncbase.UserdataSyncgroupName &&
+ // Ignore syncgroups that have already been joined.
+ !((try? Syncbase.syncgroupInUserdata(syncgroupId)) ?? false) else {
+ return
+ }
+ do {
+ let invite = SyncgroupInvite(coreInvite: invite)
+ try database().acceptSyncgroupInvite(invite, callback: { (sg, err) in
+ if err != nil {
+ print("Unable to auto-join syncgroup \(sg) with user blessings: \(err)")
+ }
+ })
+ } catch {
+ print("Unable to auto-join syncgroup \(invite.syncgroupId) with user blessings: \(error)")
+ }
+ }
+
private static func onUserdataWatchChange(changes: [WatchChange]) {
for change in changes {
guard let row = change.row where change.entityType == .Row && change.changeType == .Put else {
@@ -218,6 +257,19 @@
try userdataCollection.put(try Syncbase.UserdataCollectionPrefix + syncgroupId.encode(), value: NSData())
}
+ static func syncgroupInUserdata(syncgroupId: Identifier) throws -> Bool {
+ if !Syncbase.didInit {
+ throw SyncbaseError.NotConfigured
+ }
+ if !Syncbase.didPostLogin {
+ throw SyncbaseError.NotLoggedIn
+ }
+ guard let userdataCollection = Syncbase.userdataCollection else {
+ throw SyncbaseError.IllegalArgument(detail: "No user data collection")
+ }
+ return try userdataCollection.exists(try Syncbase.UserdataCollectionPrefix + syncgroupId.encode())
+ }
+
/// Returns the shared database handle. Must have already called `configure` and be logged in,
/// otherwise this will throw a `SyncbaseError.NotConfigured` or `SyncbaseError.NotLoggedIn`
/// error.
diff --git a/Syncbase/Source/SyncgroupInvite.swift b/Syncbase/Source/SyncgroupInvite.swift
index da353aa..6a5de4e 100644
--- a/Syncbase/Source/SyncgroupInvite.swift
+++ b/Syncbase/Source/SyncgroupInvite.swift
@@ -10,6 +10,12 @@
public let syncgroupId: Identifier
public let inviterBlessingNames: [String]
+ init(coreInvite: SyncbaseCore.SyncgroupInvite) {
+ self.init(
+ syncgroupId: Identifier(coreId: coreInvite.syncgroupId),
+ inviterBlessingNames: coreInvite.blessingNames)
+ }
+
public init(syncgroupId: Identifier, inviterBlessingNames: [String]) {
self.syncgroupId = syncgroupId
self.inviterBlessingNames = inviterBlessingNames
diff --git a/Syncbase/Tests/BasicDatabaseTests.swift b/Syncbase/Tests/BasicDatabaseTests.swift
index 3ce4e8d..4ef5fd4 100644
--- a/Syncbase/Tests/BasicDatabaseTests.swift
+++ b/Syncbase/Tests/BasicDatabaseTests.swift
@@ -5,6 +5,7 @@
import XCTest
@testable import Syncbase
import enum Syncbase.Syncbase
+import class Syncbase.Database
@testable import SyncbaseCore
let testQueue = dispatch_queue_create("SyncbaseQueue", DISPATCH_QUEUE_SERIAL)
@@ -138,6 +139,94 @@
}
}
+class InviteUserdataFilteredTest: SyncgroupTest {
+ func testUserdataFiltered() {
+ withDb { db in
+ db.addSyncgroupInviteHandler(SyncgroupInviteHandler(
+ onInvite: { invite in
+ XCTFail("Not expecting any invites")
+ },
+ onError: { err in
+ XCTFail("Not expecting any errors: \(err)")
+ }))
+ NSThread.sleepForTimeInterval(0.1)
+ db.removeAllSyncgroupInviteHandlers()
+ }
+ }
+}
+
+class InvitedSyncgroupsTest: SyncgroupTest {
+ static let coreSyncgroupId = SyncbaseCore.Identifier(
+ name: "someSg",
+ blessing: "dev.v.io:o:some.apps.googleusercontent.com:someone@google.com")
+ static let coreInvite = SyncbaseCore.SyncgroupInvite(
+ syncgroupId: coreSyncgroupId,
+ addresses: [""],
+ blessingNames: [coreSyncgroupId.blessing])
+
+ func testInvitedSyncgroupsNotIgnoredWhenNew() {
+ withDb { db in
+
+ var didGetInvite = false
+ let handler = SyncgroupInviteHandler(
+ onInvite: { invite in
+ if invite.syncgroupId.name == InvitedSyncgroupsTest.coreSyncgroupId.name {
+ didGetInvite = true
+ }
+ },
+ onError: { err in
+ XCTFail("Not expecting any errors: \(err)")
+ })
+ db.addSyncgroupInviteHandler(handler)
+ let coreHandler = Database.syncgroupInviteHandlers[handler]
+ coreHandler?.onInvite(InvitedSyncgroupsTest.coreInvite)
+ XCTAssertTrue(didGetInvite)
+ db.removeSyncgroupInviteHandler(handler)
+ }
+ }
+
+ func testInvitedSyncgroupsIgnoredWhenJoined() {
+ withDb { db in
+ // We should get the invite when it's not in our userdata
+ let coreInvite = SyncbaseCore.SyncgroupInvite(
+ syncgroupId: InvitedSyncgroupsTest.coreSyncgroupId,
+ addresses: [""],
+ blessingNames: [InvitedSyncgroupsTest.coreSyncgroupId.blessing])
+ var didGetInvite = false
+ var handler = SyncgroupInviteHandler(
+ onInvite: { invite in
+ if invite.syncgroupId == Identifier(coreId: InvitedSyncgroupsTest.coreSyncgroupId) {
+ didGetInvite = true
+ }
+ },
+ onError: { err in
+ XCTFail("Not expecting any errors: \(err)")
+ })
+ db.addSyncgroupInviteHandler(handler)
+ var coreHandler = Database.syncgroupInviteHandlers[handler]
+ coreHandler?.onInvite(coreInvite)
+ XCTAssertTrue(didGetInvite)
+ db.removeSyncgroupInviteHandler(handler)
+
+ // Add it to userdata
+ try Syncbase.addSyncgroupToUserdata(Identifier(coreId: InvitedSyncgroupsTest.coreSyncgroupId))
+
+ // Now we shouldn't get it
+ handler = SyncgroupInviteHandler(
+ onInvite: { invite in
+ XCTFail("Not expecting any invites: \(invite)")
+ },
+ onError: { err in
+ XCTFail("Not expecting any errors: \(err)")
+ })
+ db.addSyncgroupInviteHandler(handler)
+ coreHandler = Database.syncgroupInviteHandlers[handler]
+ coreHandler?.onInvite(coreInvite)
+ db.removeSyncgroupInviteHandler(handler)
+ }
+ }
+}
+
class WatchSyncgroupTest: SyncgroupTest {
func testAddingSyncgroup() {
withDb { db in