todosapp: wire up real syncgroup calls
i haven't tested sync yet, but everything works for a single user
yeeeehhhhaaaa
Change-Id: Ib0aff7f53616a749535824ee35d8ca051ff825fa
diff --git a/browser/defaults.js b/browser/defaults.js
index d41e637..2dceddb 100644
--- a/browser/defaults.js
+++ b/browser/defaults.js
@@ -84,8 +84,6 @@
var service = syncbase.newService(name);
var app = service.app('todos'), db = app.noSqlDatabase('db');
var disp = new SyncbaseDispatcher(rt, db);
- // TODO(sadovsky): Check that the VC (and discharge, etc.) for this RPC is
- // reused for all subsequent RPCs.
app.create(wt(ctx), {}, function(err) {
if (err) {
if (err instanceof verror.ExistError) {
@@ -103,6 +101,10 @@
db.createTable(wt(ctx), 'tb', {}, function(err) {
if (err) return cb(err);
if (benchmark) {
+ // TODO(sadovsky): Restructure things so that we still call initData
+ // even if on the first page load we ran the benchmark. Maybe do this
+ // by having the benchmark use a completely different app and/or
+ // database.
return bm.runBenchmark(ctx, db, cb);
}
console.log('hierarchy created; writing rows');
diff --git a/browser/index.js b/browser/index.js
index 09b69ad..dd5d88d 100644
--- a/browser/index.js
+++ b/browser/index.js
@@ -25,8 +25,8 @@
////////////////////////////////////////
// Global state
-// Dispatcher, initialized by initDispatcher.
-var disp;
+// Dispatcher and user's email address, both initialized by initDispatcher.
+var disp, userEmail;
// Used for query params.
var u = url.parse(window.location.href, true);
@@ -59,6 +59,7 @@
} else if (dispType === DISP_TYPE_SYNCBASE) {
initVanadium(function(err, rt) {
if (err) return cb(err);
+ userEmail = blessingToEmail(rt.accountName);
defaults.initSyncbaseDispatcher(rt, syncbaseName, benchmark, cb);
});
} else {
@@ -68,6 +69,16 @@
}
}
+// HACKETY HACK
+function emailToBlessing(email) {
+ return 'dev.v.io/u/' + email;
+}
+function blessingToEmail(blessing) {
+ var parts = blessing.split('/');
+ console.assert(parts.length >= 3);
+ return parts[2];
+}
+
function activateInput(input) {
input.focus();
input.select();
@@ -265,6 +276,8 @@
render: function() {
var that = this;
var children = [];
+ // TODO(sadovsky): If there are no lists (and thus no todos), we end up
+ // rendering "Loading..." here, which is wrong.
if (!this.props.listId || !this.props.todos) {
children.push(h('div.loading', {key: 'loading'}, 'Loading...'));
} else {
@@ -382,30 +395,41 @@
h('input', _.assign({
key: 'input',
placeholder: 'Add email address',
- // TODO(sadovsky): Let the user add members to an existing SG once
- // Syncbase supports it.
- disabled: shared
}, okCancelEvents({
ok: function(value, e) {
- disp.createSyncGroup(list._id);
e.target.value = '';
+ if (shared) {
+ // TODO(sadovsky): Let the user add members to an existing SG once
+ // Syncbase supports it.
+ console.error('Cannot add members to existing SyncGroup.');
+ return;
+ }
+ // TODO(sadovsky): Better input validation.
+ if (!value.includes('@') || !value.includes('.')) {
+ console.error('Invalid email address.');
+ return;
+ }
+ disp.createSyncGroup(list._id, [
+ emailToBlessing(userEmail),
+ emailToBlessing(value)
+ ]);
},
cancel: function(e) {
e.target.value = '';
}
}, true))),
- // TODO(sadovsky): Exclude self from displayed member list?
+ // TODO(sadovsky): Exclude self?
!shared ? null : h('div.shared-with', {key: 'shared-with'}, [
h('div.subtitle', {key: 'subtitle'}, 'Currently shared with'),
h('div.emails', {
key: 'emails'
- }, _.map(list.sg.members, function(member) {
- return h('div.email', {key: member}, member);
- }))
+ }, _.map(list.sg.spec.perms.get('Admin')['in'], function(blessing) {
+ return blessingToEmail(blessing);
+ }).join(', '))
]),
!shared ? null : h('div.url', {key: 'url'}, [
- h('div.subtitle', {key: 'subtitle'}, 'Url to share with invitees'),
- h('div.value', {key: 'value'}, shareUrl)
+ h('div.subtitle', {key: 'subtitle'}, 'URL to share with invitees'),
+ h('div.value', {key: 'value'}, h('a', {href: shareUrl}, shareUrl))
]),
h('div.close', {
key: 'close',
@@ -478,12 +502,8 @@
showStatusDialog: false
};
},
- getSyncGroup_: function(listId, cb) {
- console.assert(this.props.dispType === DISP_TYPE_SYNCBASE);
- disp.getSyncGroup(listId, cb);
- },
getLists_: function(cb) {
- disp.getListsWithSyncGroups(function(err, lists) {
+ disp.getLists(function(err, lists) {
if (err) return cb(err);
// Sort lists by name in the UI.
return cb(null, _.sortBy(lists, 'name'));
@@ -517,9 +537,15 @@
}
initDispatcher(dt, sn, bm, function(err) {
if (err) throw err;
+ if (bm) {
+ console.log('benchmark done');
+ return;
+ }
if (props.joinListId) {
console.assert(dt === DISP_TYPE_SYNCBASE);
disp.joinSyncGroup(props.joinListId, function(err) {
+ // Note, joinSyncGroup is a noop (no error) if the caller is already a
+ // member, which is the desired behavior here.
if (err) throw err;
done();
});
@@ -597,12 +623,13 @@
// TODO(sadovsky): Only read (and only redraw) what's needed based on what
// changed.
disp.on('change', function() {
+ var doneCb = bm.logFn('onChange', function(err) {
+ if (err) throw err;
+ });
updateLists(function(err) {
if (err) throw err;
var listId = getListId();
- updateTodos(listId, function(err) {
- if (err) throw err;
- });
+ updateTodos(listId, doneCb);
});
});
diff --git a/browser/syncbase_dispatcher.js b/browser/syncbase_dispatcher.js
index bc51adb..89ac9b3 100644
--- a/browser/syncbase_dispatcher.js
+++ b/browser/syncbase_dispatcher.js
@@ -3,9 +3,8 @@
// Schema design doc (a bit outdated):
// https://docs.google.com/document/d/1GtBk75QmjSorUW6T6BATCoiS_LTqOrGksgqjqJ1Hiow/edit#
//
-// TODO(sadovsky): Currently, list and todo order are not preserved. We should
-// make the app always order these lexicographically, or better yet, use a
-// Dewey-Decimal-like scheme (with randomization).
+// TODO(sadovsky): Support arbitrary item reordering using a Dewey-Decimal-like
+// order-tracking scheme, with suffix randomization to prevent conflicts.
//
// NOTE: For now, when an item is deleted, any sub-items that were added
// concurrently (on some other device) are orphaned. Eventually, we'll GC
@@ -14,10 +13,7 @@
// stored in Syncbase.
//
// TODO(sadovsky): Orphaning degrades performance, because scan responses (e.g.
-// scan to get all lists) include orphaned records. If we switch from scans to
-// queries, performance should improve since all row filtering will happen
-// server side.
-// TODO(sadovsky): Better yet, move lists to a separate keyspace.
+// scan to get all todos for a given list) include orphaned records.
'use strict';
@@ -60,7 +56,7 @@
function join() {
// TODO(sadovsky): Switch to using naming.join() once Syncbase allows slashes
- // in row keys. Also, restrict which chars are allowed in tags.
+ // in row keys. Also, restrict which chars are allowed in tag names.
var args = Array.prototype.slice.call(arguments);
return args.join(SEP);
}
@@ -91,7 +87,7 @@
throw new Error('bad key: ' + key);
}
-// TODO(sadovsky): Switch from JSON to VOM.
+// TODO(sadovsky): Maybe switch from JSON to VOM/VDL.
function marshal(x) {
return JSON.stringify(x);
}
@@ -144,9 +140,10 @@
////////////////////////////////////////
// SyncbaseDispatcher impl
-// TODO(sadovsky): Use a query for better performance. In theory it's possible
-// to query based on row key regexp.
-define('getLists', function(ctx, cb) {
+// Returns list objects without 'sg' fields.
+// TODO(sadovsky): Use a query for better performance. It should be possible to
+// query based on row key regexp.
+define('getListsOnly_', function(ctx, cb) {
this.getRows_(ctx, null, function(err, rows) {
if (err) return cb(err);
var lists = [];
@@ -160,29 +157,27 @@
});
});
-define('getListsWithSyncGroups', function(ctx, cb) {
+// Returns a list of objects describing SyncGroups.
+// TODO(sadovsky): Would be nice if this could be done with a single RPC.
+define('getSyncGroups_', function(ctx, cb) {
+ var that = this;
+ this.db_.getSyncGroupNames(ctx, function(err, sgNames) {
+ if (err) return cb(err);
+ async.map(sgNames, function(sgName, cb) {
+ that.getSyncGroup_(ctx, that.sgNameToListId_(sgName), cb);
+ }, cb);
+ });
+});
+
+// Returns list objects with 'sg' fields.
+define('getLists', function(ctx, cb) {
var that = this;
async.parallel([
function(cb) {
- that.getLists(ctx, cb);
+ that.getListsOnly_(ctx, cb);
},
- // Returns a list of SG objects (from getSyncGroup), one per SG.
- // TODO(sadovsky): Would be nice if Syncbase could provide more info in a
- // single RPC.
function(cb) {
- bm.logFn('getSyncGroupNames', cb);
- // FIXME: Remove this hack once db.getSyncGroupNames, sg.getSpec, and
- // sg.getMembers are implemented.
- /* jshint -W027 */
- return process.nextTick(function() {
- cb(null, []);
- });
- that.db_.getSyncGroupNames(ctx, function(err, sgNames) {
- if (err) return cb(err);
- async.map(sgNames, function(sgName, cb) {
- that.getSyncGroup(ctx, that.sgNameToListId_(sgName), cb);
- }, cb);
- });
+ that.getSyncGroups_(ctx, cb);
}
], function(err, results) {
if (err) return cb(err);
@@ -191,8 +186,8 @@
_.forEach(lists, function(list) {
var listId = list._id;
_.forEach(sgs, function(sg) {
- console.assert(sg.spec.Prefixes.length === 1);
- if (listId === sg.spec.Prefixes[0]) {
+ console.assert(sg.spec.prefixes.length === 1);
+ if (listId === sg.spec.prefixes[0].slice(3)) { // drop 'tb:' prefix
list.sg = sg;
}
});
@@ -306,32 +301,43 @@
// We use <app>/<db>/<table>/<listId> for the suffix part.
SyncbaseDispatcher.prototype.sgNameToListId_ = function(sgName) {
- return sgName.replace(new RegExp('.*/$sync/todos/db/tb/'), '');
+ return sgName.replace(new RegExp('.*/\\$sync/todos/db/tb/'), '');
};
SyncbaseDispatcher.prototype.listIdToSgName_ = function(listId) {
- var prefix = this.tb_.fullName.replace('/todos/db/tb/',
- '/$sync/todos/db/tb/');
- return prefix + listId;
+ var prefix = this.tb_.fullName.replace('/todos/db/tb',
+ '/$sync/todos/db/tb');
+ return prefix + '/' + listId;
};
-// Returns spec and members for the given list.
-define('getSyncGroup', function(ctx, listId, cb) {
- var sg = this.db_.syncGroup(this.listIdToSgName_(listId));
+// Returns an object describing the SyncGroup for the given list id.
+// Currently, this object will have just one field, 'spec'.
+define('getSyncGroup_', function(ctx, listId, cb) {
+ var sgName = this.listIdToSgName_(listId), sg = this.db_.syncGroup(sgName);
async.parallel([
function(cb) {
- sg.getSpec(ctx, cb);
+ sg.getSpec(ctx, function(err, spec, version) {
+ if (err) return cb(err);
+ cb(null, spec);
+ });
},
function(cb) {
- sg.getMembers(ctx, cb);
+ // TODO(sadovsky): For now, in the UI we just want to show who's on the
+ // ACL for a given list, so we don't bother with getMembers. On top of
+ // that, currently getMembers returns a map of random Syncbase instance
+ // ids to SyncGroupMemberInfo structs, neither of which tell us anything
+ // useful.
+ if (true) {
+ process.nextTick(cb);
+ } else {
+ sg.getMembers(ctx, cb);
+ }
}
], function(err, results) {
if (err) return cb(err);
- // FIXME: Convert 'members' to email addresses.
- return {
+ cb(null, {
spec: results[0],
- members: _.keys(results[1])
- };
+ });
});
});
@@ -341,16 +347,16 @@
syncPriority: 8
});
-define('createSyncGroup', function(ctx, listId, cb) {
- var sg = this.db_.syncGroup(this.listIdToSgName_(listId));
+define('createSyncGroup', function(ctx, listId, blessings, cb) {
+ var sgName = this.listIdToSgName_(listId), sg = this.db_.syncGroup(sgName);
var spec = new nosql.SyncGroupSpec({
- // TODO(sadovsky): Make perms more restrictive.
+ // TODO(sadovsky): Maybe make perms more restrictive.
perms: new Map([
- ['Admin', {'In': ['...']}],
- ['Read', {'In': ['...']}],
- ['Write', {'In': ['...']}],
- ['Resolve', {'In': ['...']}],
- ['Debug', {'In': ['...']}]
+ ['Admin', {'in': blessings}],
+ ['Read', {'in': blessings}],
+ ['Write', {'in': blessings}],
+ ['Resolve', {'in': blessings}],
+ ['Debug', {'in': blessings}]
]),
// TODO(sadovsky): Update this once we switch to {table, prefix} tuples.
prefixes: ['tb:' + listId]
@@ -359,7 +365,7 @@
});
define('joinSyncGroup', function(ctx, listId, cb) {
- var sg = this.db_.syncGroup(this.listIdToSgName_(listId));
+ var sgName = this.listIdToSgName_(listId), sg = this.db_.syncGroup(sgName);
sg.join(ctx, MEMBER_INFO, this.maybeEmit_(cb));
});
@@ -369,6 +375,8 @@
// DO NOT USE THIS. vtrace RPCs are extremely slow in JavaScript because VOM
// decoding is slow for trace records, which are deeply nested. E.g. 100 puts
// can take 20+ seconds with vtrace vs. 2 seconds without.
+// EDIT: It might not be quite that bad - the "20+ seconds" cited above might
+// also include the latency added by having the Chrome dev console open.
SyncbaseDispatcher.prototype.resetTraceRecords = function() {
this.ctx_ = vtrace.withNewStore(this.rt_.getContext());
vtrace.getStore(this.ctx_).setCollectRegexp('.*');
@@ -397,7 +405,7 @@
}
var seq = 0; // for our own writes
-var prevWatchVersions = {}; // map of list id to version string
+var prevVersions = null; // map of list id to version string
// Increments stored seq for this client.
define('bumpSeq_', function(ctx, listId, cb) {
@@ -422,10 +430,10 @@
// Returns true if any data has arrived via sync, false if not.
define('checkForChanges_', function(ctx, cb) {
var that = this;
- this.getLists(ctx, function(err, lists) {
+ this.getListsOnly_(ctx, function(err, lists) {
if (err) return cb(err);
// Build a map of list id to current version.
- var currWatchVersions = {};
+ var currVersions = {};
var listIds = _.pluck(lists, '_id');
async.each(listIds, function(listId, cb) {
that.getWatchSeqMap_(ctx, listId, function(err, seqMap) {
@@ -435,14 +443,14 @@
var strs = _.map(seqMap, function(v, k) {
return k + ':' + v;
});
- currWatchVersions[listId] = strs.sort().join(',');
+ currVersions[listId] = strs.sort().join(',');
cb();
});
}, function(err) {
if (err) return cb(err);
// Note that _.isEqual performs a deep comparison.
- var changed = !_.isEqual(currWatchVersions, prevWatchVersions);
- prevWatchVersions = currWatchVersions;
+ var changed = prevVersions && !_.isEqual(currVersions, prevVersions);
+ prevVersions = currVersions;
cb(null, changed);
});
});
@@ -467,8 +475,7 @@
////////////////////////////////////////
// Internal helpers
-// TODO(sadovsky): Watch for changes on Syncbase itself so that we can detect
-// when data arrives via sync, and drop this method.
+// TODO(sadovsky): Drop this method once we have client watch.
SyncbaseDispatcher.prototype.maybeEmit_ = function(cb, key) {
var that = this;
cb = cb || noop;
diff --git a/stylesheets/constants.less b/stylesheets/constants.less
index 8e996ff..8f36820 100644
--- a/stylesheets/constants.less
+++ b/stylesheets/constants.less
@@ -1,3 +1,4 @@
/* https://www.google.com/design/spec/style/color.html */
@color-green-700: #388e3c;
+@color-red-500: #f44336;
@color-red-700: #d32f2f;
diff --git a/stylesheets/index.less b/stylesheets/index.less
index 0d9f70f..468e796 100644
--- a/stylesheets/index.less
+++ b/stylesheets/index.less
@@ -46,6 +46,8 @@
input {
font: inherit;
+ margin: 0;
+ padding: 0;
&:focus {
outline: none;
@@ -156,7 +158,7 @@
background-color: #888;
&.shared {
- background-color: @color-green-700;
+ background-color: @color-red-500;
}
}
}
@@ -195,24 +197,36 @@
background-color: #fff;
border: 1px solid rgba(0, 0, 0, 0.3);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2);
+ }
- input {
- width: 50%;
- }
+ input {
+ width: 70%;
+ }
- .close {
- position: absolute;
- top: 0;
- right: 0;
- margin: 8px;
- background: url("/public/destroy.png") no-repeat 0 0;
- cursor: pointer;
- height: 20px;
- width: 20px;
+ .shared-with, .url {
+ margin-top: 24px;
+ }
- &:hover {
- background-position: 0 -20px;
- }
+ .subtitle {
+ margin-bottom: 4px;
+ color: #999;
+ font-size: 13px;
+ font-weight: bold;
+ text-transform: uppercase;
+ }
+
+ .close {
+ position: absolute;
+ top: 0;
+ right: 0;
+ margin: 8px;
+ background: url("/public/destroy.png") no-repeat 0 0;
+ cursor: pointer;
+ height: 20px;
+ width: 20px;
+
+ &:hover {
+ background-position: 0 -20px;
}
}
}