First pass of working invitations
Added dump debug method
Adding per-trip sync groups (TODO: lazy create these)
Adding SyncGroup deletion wrapper (unused now)
Removing per-sycngroup sgmt instances (significant performance improvement)
Change-Id: Ie8bf588f7e08feff09c6577b2dc5d6bfe380ef96
diff --git a/src/invitation-manager.js b/src/invitation-manager.js
index e1a26a4..fc98c76 100644
--- a/src/invitation-manager.js
+++ b/src/invitation-manager.js
@@ -6,6 +6,7 @@
var $ = require('./util/jquery');
var defineClass = require('./util/define-class');
+var debug = require('./debug');
// TODO(rosswang): generalize this
var ESC = {
@@ -31,42 +32,24 @@
});
}
-function invitationKey(owner, recipient) {
- return ['invitations', escapeUsername(owner), escapeUsername(recipient)];
+function invitationKey(recipient, owner, tripId) {
+ return [
+ 'invitations',
+ escapeUsername(recipient),
+ escapeUsername(owner),
+ tripId
+ ];
}
var InvitationManager = defineClass({
publics: {
- accept: function(owner) {
- },
-
- decline: function(owner) {
- return Promise.all([
- this.syncbasePromise,
- this.prereqs
- ]).then(function(args) {
- var syncbase = args[0];
- var username = args[1].identity.username;
-
- return syncbase.delete(invitationKey(owner, username));
- });
- },
-
- getActiveInvite: function() {
- return this.activeInvite;
- },
-
- invite: function(username) {
+ invite: function(recipient, owner, tripId) {
var self = this;
return this.groupManagerPromise.then(function(gm) {
- return gm.joinSyncGroup(username, 'invitations').then(function() {
- return self.prereqs;
- }).then(function(prereqs) {
- var owner = self.activeInvite || prereqs.identity.username;
-
- return gm.syncbaseWrapper.put(invitationKey(owner, username),
- prereqs.identity.username);
+ return gm.joinSyncGroup(recipient, 'invitations').then(function() {
+ return gm.syncbaseWrapper.put(invitationKey(recipient, owner, tripId),
+ self.username);
});
});
},
@@ -77,55 +60,93 @@
},
privates: {
+ invitation: defineClass.innerClass({
+ publics: {
+ delete: function() {
+ var self = this;
+
+ var username = this.outer.username;
+ return this.outer.syncbasePromise.then(function(syncbase) {
+ return syncbase.delete(invitationKey(
+ username, self.owner, self.tripId));
+ });
+ }
+ },
+
+ constants: [ 'owner', 'tripId', 'sender' ],
+
+ events: {
+ onDismiss: 'memory once'
+ },
+
+ init: function(owner, tripId, sender, callbacks) {
+ this.owner = owner;
+ this.tripId = tripId;
+ this.sender = sender;
+ callbacks.dismiss = this.onDismiss;
+ }
+ }),
+
processUpdates: function(data) {
var self = this;
- if (data.invitations) {
- $.each(data.invitations, function(owner, record) {
+ var toMe;
+ if (data.invitations &&
+ (toMe = data.invitations[escapeUsername(this.username)])) {
+ $.each(toMe, function(owner, ownerRecords) {
var ownerInvites = self.invitations[owner];
if (!ownerInvites) {
ownerInvites = self.invitations[owner] = {};
}
- $.each(record, function(recipient, sender) {
- if (ownerInvites[recipient]) {
- delete ownerInvites[recipient];
+ var uOwner;
+
+ $.each(ownerRecords, function(tripId, sender) {
+ var record = ownerInvites[tripId];
+ if (record) {
+ record.seen = true;
} else {
- self.onInvite(unescapeUsername(owner),
- unescapeUsername(recipient), sender);
+ if (!uOwner) {
+ uOwner = unescapeUsername(owner);
+ }
+
+ debug.log('Received invite from ' + sender + ' to ' + uOwner +
+ ':' + tripId);
+
+ var callbacks = {};
+ var invite = self.invitation(uOwner, tripId, sender, callbacks);
+ ownerInvites[tripId] = {
+ invite: invite,
+ dismiss: callbacks.dismiss,
+ seen: true
+ };
+ self.onInvite(invite);
}
});
});
}
if (this.invitations) {
- $.each(this.invitations, function(owner, record) {
- $.each(record, function(recipient, sender) {
- self.onDismiss(unescapeUsername(owner),
- unescapeUsername(recipient), sender);
+ $.each(this.invitations, function(owner, ownerRecords) {
+ $.each(ownerRecords, function(tripId, record) {
+ if (record.seen) {
+ delete record.seen;
+ } else {
+ delete ownerRecords[tripId];
+ record.dismiss();
+ }
});
});
}
-
- this.invitations = data.invitations || {};
}
},
events: {
/**
- * @param owner the user who owns the trip.
- * @param recipient the user invited to the trip.
- * @param sender the user who sent the invitation.
+ * @param invitation
*/
onInvite: '',
- /**
- * @param owner the user who owns the trip.
- * @param recipient the user invited to the trip.
- * @param sender the user who sent the invitation.
- */
- onDismiss: '',
-
onError: 'memory'
},
@@ -145,11 +166,13 @@
this.invitations = {};
prereqs.then(function(args) {
+ //this will have been set prior to groupManagerPromise completing
self.username = args.identity.username;
});
groupManagerPromise.then(function(gm) {
- gm.createSyncGroup('invitations', ['invitations'])
+ gm.createSyncGroup('invitations',
+ [['invitations', escapeUsername(self.username)]])
.catch(self.onError);
});
}
diff --git a/src/static/index.css b/src/static/index.css
index 46adc30..b6d591e 100644
--- a/src/static/index.css
+++ b/src/static/index.css
@@ -144,6 +144,10 @@
text-indent: -.5em;
}
+.messages span {
+ text-indent: initial;
+}
+
.messages.headlines li {
background-color: rgba(0, 0, 0, .6);
border-radius: 4px;
@@ -156,7 +160,6 @@
border-radius: 2px;
display: inline-block;
padding: 1px 3px;
- text-indent: initial;
}
.messages.headlines li.history {
diff --git a/src/strings.js b/src/strings.js
index 5ce352c..96f726d 100644
--- a/src/strings.js
+++ b/src/strings.js
@@ -83,6 +83,7 @@
},
'Timeline': 'Timeline',
'Travel Planner': 'Travel Planner',
+ 'Trip is still initializing.' : 'Trip is still initializing.',
'Unknown error': 'Unknown error'
};
}
diff --git a/src/syncgroup-manager.js b/src/syncgroup-manager.js
index 97b8040..35ce0c6 100644
--- a/src/syncgroup-manager.js
+++ b/src/syncgroup-manager.js
@@ -17,7 +17,7 @@
return this.prereq.then(function() {
var sg = self.syncbaseWrapper.syncGroup(self.sgAdmin, name);
- var mgmt = vanadium.naming.join(self.mountNames.app, 'sgmt', name);
+ var mgmt = vanadium.naming.join(self.mountNames.app, 'sgmt');
var spec = sg.buildSpec(prefixes, [mgmt]);
/* TODO(rosswang): Right now, duplicate Syncbase creates on
@@ -30,10 +30,16 @@
['Read', {in: ['...']}],
['Resolve', {in: ['...']}]
]));
+ }).then(function() {
+ return sg;
});
});
},
+ destroySyncGroup: function(name) {
+ return this.syncbaseWrapper.syncGroup(this.sgAdmin, name).destroy();
+ },
+
joinSyncGroup: function(owner, name) {
var sg = this.syncbaseWrapper.syncGroup(
vanadium.naming.join(naming.appMount(owner), 'sgadmin'), name);
@@ -89,4 +95,4 @@
}
});
-module.exports = SyncgroupManager;
\ No newline at end of file
+module.exports = SyncgroupManager;
diff --git a/src/travel.js b/src/travel.js
index 510c0c1..197d3e1 100644
--- a/src/travel.js
+++ b/src/travel.js
@@ -22,6 +22,7 @@
var vanadiumWrapperDefault = require('./vanadium-wrapper');
+var debug = require('./debug');
var describeDestination = require('./describe-destination');
var naming = require('./naming');
var strings = require('./strings').currentLocale;
@@ -77,6 +78,14 @@
var Travel = defineClass({
publics: {
+ dump: function() {
+ this.sync.getData().then(function(data) {
+ debug.log(data);
+ }, function(err) {
+ console.error(err);
+ });
+ },
+
error: function (err) {
this.messages.push(Message.error(err));
},
@@ -87,6 +96,32 @@
text: info,
promise: promise
}));
+ },
+
+ invite: function(recipient) {
+ var self = this;
+
+ var owner = this.sync.getActiveTripOwner();
+ if (owner) {
+ this.info(strings.sendingInvite(recipient),
+ this.sync.invitationManager.invite(recipient,
+ this.sync.getActiveTripOwner(), this.sync.getActiveTripId())
+ .then(function() {
+ var me = self.sync.invitationManager.getUsername();
+ self.sync.message({
+ type: Message.INFO,
+ text: strings.invitationSent(recipient, me)
+ });
+ }, function(err) {
+ if (err.id === 'v.io/v23/verror.NoServers') {
+ throw strings.notReachable(recipient);
+ } else {
+ throw err;
+ }
+ }));
+ } else {
+ this.error(strings['Trip is still initializing.']);
+ }
}
},
@@ -280,56 +315,39 @@
}
},
- dismissInvite: function(owner, sender) {
- var invite = this.invites[owner];
- if (invite) {
- invite.resolve(strings.invitationDismissed(sender, owner));
- delete this.invites[owner];
- }
- },
-
- handleInvite: function(owner, recipient, sender) {
+ handleInvite: function(invitation) {
var self = this;
- var invitationManager = this.sync.invitationManager;
- var me = invitationManager.getUsername();
- if (recipient === me) {
- this.dismissInvite(owner, sender);
+ var sender = invitation.sender;
+ var owner = invitation.owner;
+ var tripId = invitation.tripId;
- var message = new Message();
- message.setType(Message.INFO);
- message.setHtml(strings.invitationReceived(sender, owner));
- message.setPromise(new Promise(function(resolve, reject) {
- self.invites[owner] = {
- resolve: resolve,
- reject: reject
- };
-
- message.$.find('a[name=accept]').click(function() {
- invitationManager.accept(owner).then(function() {
- delete self.invites[owner];
+ var message = new Message();
+ message.setType(Message.INFO);
+ message.setHtml(strings.invitationReceived(sender, owner));
+ message.setPromise(new Promise(function(resolve, reject) {
+ message.$.find('a[name=accept]').click(function() {
+ self.sync.joinTripSyncGroup(owner, tripId)
+ .then(invitation.delete)
+ .then(function() {
+ self.sync.watchForTrip(tripId);
return strings.invitationAccepted(sender, owner);
}).then(resolve, reject);
- return false;
- });
- message.$.find('a[name=decline]').click(function() {
- invitationManager.decline(owner).then(function() {
- delete self.invites[owner];
- return strings.invitationDeclined(sender, owner);
- }).then(resolve, reject);
- return false;
- });
- }));
+ return false;
+ });
+ message.$.find('a[name=decline]').click(function() {
+ invitation.delete().then(function() {
+ return strings.invitationDeclined(sender, owner);
+ }).then(resolve, reject);
+ return false;
+ });
- this.messages.push(message);
- }
- },
+ invitation.onDismiss.add(function() {
+ resolve(strings.invitationDismissed(sender, owner));
+ });
+ }));
- handleInviteDismiss: function(owner, recipient, sender) {
- var me = this.sync.invitationManager.getUsername();
- if (recipient === me) {
- this.dismissInvite(owner, sender);
- }
+ this.messages.push(message);
},
handleUserMessage: function(message, raw) {
@@ -378,8 +396,6 @@
opts = opts || {};
var vanadiumWrapper = opts.vanadiumWrapper || vanadiumWrapperDefault;
- this.invites = {};
-
var destinations = this.destinations = new Destinations();
destinations.onAdd.add(this.handleDestinationAdd);
destinations.onRemove.add(this.handleDestinationRemove);
@@ -439,7 +455,6 @@
});
sync.invitationManager.onInvite.add(this.handleInvite);
- sync.invitationManager.onDismiss.add(this.handleInviteDismiss);
messages.onMessage.add(this.handleUserMessage);
@@ -524,23 +539,7 @@
this.commands = {
invite: {
- op: function(username) {
- this.info(strings.sendingInvite(username),
- this.sync.invitationManager.invite(username)
- .then(function() {
- var me = self.sync.invitationManager.getUsername();
- self.sync.message({
- type: Message.INFO,
- text: strings.invitationSent(username, me)
- });
- }, function(err) {
- if (err.id === 'v.io/v23/verror.NoServers') {
- throw strings.notReachable(username);
- } else {
- throw err;
- }
- }));
- }
+ op: this.invite
},
status: {
diff --git a/src/travelsync.js b/src/travelsync.js
index 3fedd67..4c87344 100644
--- a/src/travelsync.js
+++ b/src/travelsync.js
@@ -61,7 +61,15 @@
pushStatus: function() {
},
- setActiveTrip: function(tripId) {
+ getActiveTripId: function() {
+ return this.activeTripId;
+ },
+
+ getActiveTripOwner: function() {
+ return this.activeTripOwner;
+ },
+
+ setActiveTripId: function(tripId, pull) {
var self = this;
this.activeTripId = tripId;
@@ -71,7 +79,26 @@
syncbase.put(['user', 'tripMetadata', tripId, 'latestSwitch'],
Date.now()).catch(self.onError);
- return syncbase.refresh();
+ return pull? syncbase.refresh() : Promise.resolve();
+ });
+ },
+
+ getData: function() {
+ return this.startSyncbase.then(function(syncbase) {
+ return syncbase.getData();
+ });
+ },
+
+ /**
+ * Sets the active trip to the given trip ID after it is available.
+ */
+ watchForTrip: function(tripId) {
+ this.awaitedTripId = tripId;
+ },
+
+ joinTripSyncGroup: function(owner, tripId) {
+ return this.startSyncgroupManager.then(function(gm) {
+ return gm.joinSyncGroup(owner, 'trip-' + tripId);
});
}
},
@@ -484,24 +511,68 @@
this.getDestinationIds(trip.destinations).length <= 1;
},
+ manageTripSyncGroups: function(trips) {
+ var self = this;
+
+ //TODO(rosswang): maybe make this more intelligent, and handle ejection
+ if (trips) {
+ $.each(trips, function(tripId, trip) {
+ /* Join is idempotent, but repeatedly joining might be causing major,
+ * fatal sluggishness. TODO(rosswang): if this is not the case, maybe
+ * go ahead and poll. */
+ if (!self.joinedTrips.has(tripId) && trip.owner) {
+ self.joinedTrips.add(tripId);
+ self.joinTripSyncGroup(trip.owner, tripId).catch(self.onError);
+ }
+ });
+ }
+ },
+
processTrips: function(userTripMetadata, trips) {
+ var self = this;
+
+ this.manageTripSyncGroups(trips);
+
var trip;
+ if (this.awaitedTripId) {
+ this.setActiveTripId(this.awaitedTripId, false);
+ delete this.awaitedTripId;
+
+ /* Override latestSwitch this frame. (Subsequently syncbase will be up
+ * to date.) */
+ if (!userTripMetadata) {
+ userTripMetadata = {};
+ }
+ var activeTripMd = userTripMetadata[this.activeTripId];
+ if (!activeTripMd) {
+ activeTripMd = userTripMetadata[this.activeTripId] = {};
+ }
+ activeTripMd.latestSwitch = Date.now();
+ }
+
if (this.activeTripId) {
trip = trips && trips[this.activeTripId];
if (!trip) {
debug.log('Last active trip ' + this.activeTripId +
' is no longer present.');
- } else if (this.isNascent(trip)) {
- var establishedId = this.getDefaultTrip(userTripMetadata, trips);
- if (establishedId && establishedId !== this.activeTripId &&
- trips[establishedId]) {
- this.deleteTrip(this.activeTripId);
+ } else {
+ var defaultId = this.getDefaultTrip(userTripMetadata, trips);
+ if (defaultId && defaultId !== this.activeTripId &&
+ trips[defaultId]) {
+ if (this.isNascent(trip)) {
+ this.deleteTrip(this.activeTripId);
+ debug.log('Replacing nascent trip ' + this.activeTripId +
+ ' with established trip ' + defaultId);
+ } else {
+ /* TODO(rosswang): for now, sync trip changes. This behavior may
+ * change. */
+ debug.log('Replacing active trip ' + this.activeTripId +
+ ' with most recent selection ' + defaultId);
+ }
- debug.log('Replacing nascent trip ' + this.activeTripId +
- ' with established trip ' + establishedId);
- this.activeTripId = establishedId;
- trip = trips[establishedId];
+ this.activeTripId = defaultId;
+ trip = trips[defaultId];
}
}
}
@@ -512,12 +583,23 @@
debug.log('Setting active trip ' + this.activeTripId);
trip = trips[this.activeTripId];
} else {
- this.activeTripId = uuid.v4();
- debug.log('Creating new trip ' + this.activeTripId);
- trip = {};
+ var tripId = this.activeTripId = uuid.v4();
+ debug.log('Creating new trip ' + tripId);
+ trip = {}; //don't initialize owner until the syncgroup is ready
+ this.startSyncgroupManager.then(function(gm) {
+ return self.createTripSyncGroup(gm, tripId)
+ .then(function(sg) {
+ return gm.syncbaseWrapper.put(['trips', tripId, 'owner'],
+ self.invitationManager.getUsername()).then(function() {
+ return sg;
+ });
+ })
+ .catch(self.onError);
+ });
}
}
+ this.activeTripOwner = trip.owner;
this.processMessages(trip.messages);
this.processDestinations(trip.destinations);
},
@@ -585,15 +667,20 @@
createPrimarySyncGroup: function(groupManager) {
var self = this;
- this.status.tripSyncGroup = 'creating';
- return groupManager.createSyncGroup('trip', [''])
+ this.status.userSyncGroup = 'creating';
+ return groupManager.createSyncGroup('user', [[]])
.then(function(sg) {
- self.status.tripSyncGroup = 'created';
+ self.status.userSyncGroup = 'created';
return sg;
}, function(err) {
- self.status.tripSyncGroup = 'failed';
+ self.status.usersSyncGroup = 'failed';
throw err;
});
+ },
+
+ createTripSyncGroup: function(groupManager, tripId) {
+ return groupManager.createSyncGroup('trip-' + tripId,
+ [['trips', tripId]]);
}
},
@@ -636,22 +723,23 @@
this.messages = {};
this.destRecords = [];
this.status = {};
+ this.joinedTrips = new Set();
this.server = new vdlTravel.TravelSync();
var startRpc = prereqs.then(this.serve);
var startSyncbase = this.startSyncbase = prereqs.then(this.connectSyncbase);
- var startSyncgroupManager = Promise
+ this.startSyncgroupManager = Promise
.all([prereqs, startSyncbase])
.then(function(args) {
return self.createSyncgroupManager(args[0], args[1]);
});
- var createPrimarySyncGroup = startSyncgroupManager
+ var createPrimarySyncGroup = this.startSyncgroupManager
.then(this.createPrimarySyncGroup);
this.startup = Promise.all([
startRpc,
startSyncbase,
- startSyncgroupManager,
+ this.startSyncgroupManager,
createPrimarySyncGroup
]).then(function(values) {
return {
@@ -662,7 +750,7 @@
});
this.invitationManager = new InvitationManager(prereqs,
- startSyncgroupManager);
+ this.startSyncgroupManager);
this.invitationManager.onError.add(this.onError);
this.handleDestinationPlaceChange = function() {
diff --git a/src/vanadium-wrapper/syncbase-wrapper.js b/src/vanadium-wrapper/syncbase-wrapper.js
index 92ba622..9b7664e 100644
--- a/src/vanadium-wrapper/syncbase-wrapper.js
+++ b/src/vanadium-wrapper/syncbase-wrapper.js
@@ -141,6 +141,10 @@
return this.manageWrite(this.standardDelete(this.deleteFromSyncbase, k));
},
+ getData: function() {
+ return this.data;
+ },
+
/**
* Since I/O is asynchronous, sparse, and fast, let's avoid concurrency/
* merging with the local syncbase instance by only starting a refresh if
@@ -163,6 +167,7 @@
if (!current) {
current = this.pull.current = this.pull().then(function(data) {
self.pull.current = null;
+ self.data = data;
self.onUpdate(data);
return data;
}, function(err) {
@@ -194,6 +199,11 @@
sg.create(self.context, spec, SG_MEMBER_INFO, chainable(cb));
});
+ var destroy = promisify(function(cb) {
+ debug.log('Syncbase: destroy syncgroup ' + name);
+ sg.destroy(self.context, cb);
+ });
+
var join = promisify(function(cb) {
debug.log('Syncbase: join syncgroup ' + name);
sg.join(self.context, SG_MEMBER_INFO, chainable(cb));
@@ -203,7 +213,10 @@
sg.setSpec(self.context, spec, '', chainable(cb));
});
- //be explicit about arg lists because promisify is sensitive to extra args
+ /* Be explicit about arg lists because promisify is sensitive to extra
+ * args. i.e. even though destroy and join could just be fn refs, since
+ * they're made by promisify, wrap them in a fn that actually takes 0
+ * args. */
sgp = {
buildSpec: function(prefixes, mountTables) {
return new syncbase.nosql.SyncGroupSpec({
@@ -214,12 +227,13 @@
['Resolve', {in: ['...']}],
['Debug', {in: ['...']}]
]),
- prefixes: prefixes.map(function(p) { return 't:' + p; }),
+ prefixes: prefixes.map(function(p) { return 't:' + joinKey(p); }),
mountTables: mountTables
});
},
create: function(spec) { return create(spec); },
+ destroy: function() { return destroy(); },
join: function() { return join(); },
setSpec: function(spec) { return setSpec(spec); },