todosapp: wire up syncbase

The good news: it works!
The bad news: it's dog slow.

Other notes:
- I realized that it makes more sense to plug in Syncbase at
  the Dispatcher level, rather than have it implement the
  Collection API. It was a pretty quick change.
- I sprinkled in various "NOTE" comments in places where I
  found the Syncbase API to be deficient or awkward.

Change-Id: I9d7a10f11ed3eae48e2126af916cec85c9b7e9d2
diff --git a/.gitignore b/.gitignore
index 8758413..aa673e6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,4 @@
-go/bin
+bin
 node_modules
 public/bundle.*
 tmp
diff --git a/Makefile b/Makefile
index ff687a8..1c0fd6b 100644
--- a/Makefile
+++ b/Makefile
@@ -1,24 +1,42 @@
 SHELL := /bin/bash -euo pipefail
-export PATH := go/bin:node_modules/.bin:$(V23_ROOT)/release/go/bin:$(V23_ROOT)/roadmap/go/bin:$(V23_ROOT)/third_party/cout/node/bin:$(PATH)
+export PATH := node_modules/.bin:$(V23_ROOT)/release/go/bin:$(V23_ROOT)/roadmap/go/bin:$(V23_ROOT)/third_party/cout/node/bin:$(PATH)
 
+# Default browserify options: use sourcemaps.
+BROWSERIFY_OPTS := --debug
+# Names that should not be mangled by minification.
+RESERVED_NAMES := 'context,ctx,callback,cb,$$stream,serverCall'
+# Don't mangle RESERVED_NAMES, and screw ie8.
+MANGLE_OPTS := --mangle [--except $(RESERVED_NAMES) --screw_ie8]
+# Don't remove unused variables from function arguments, which could mess up
+# signatures. Also don't evaulate constant expressions, since we rely on them to
+# conditionally require modules only in node.
+COMPRESS_OPTS := --compress [--no-unused --no-evaluate]
+# Workaround for Browserify opening too many files: increase the limit on file
+# descriptors.
+# https://github.com/substack/node-browserify/issues/431
+INCREASE_FILE_DESC = ulimit -S -n 2560
+
+# Browserify and extract sourcemap, but do not minify.
 define BROWSERIFY
 	mkdir -p $(dir $2)
-	browserify $1 -d -o $2
+	$(INCREASE_FILE_DESC); \
+	browserify $1 $(BROWSERIFY_OPTS) | exorcist $2.map > $2
 endef
 
+# Browserify, minify, and extract sourcemap.
 define BROWSERIFY_MIN
 	mkdir -p $(dir $2)
-	browserify $1 -d -p [minifyify --map $(notdir $2).map --output $2.map] -o $2
+	$(INCREASE_FILE_DESC); \
+	browserify $1 $(BROWSERIFY_OPTS) --g [uglifyify $(MANGLE_OPTS) $(COMPRESS_OPTS)] | exorcist $2.map > $2
 endef
 
 .DELETE_ON_ERROR:
 
-go/bin: $(shell find $(V23_ROOT) -name "*.go")
+bin: $(shell find $(V23_ROOT) -name "*.go")
 	v23 go build -a -o $@/principal v.io/x/ref/cmd/principal
-	v23 go build -a -tags wspr -o $@/servicerunner v.io/x/ref/cmd/servicerunner
 	v23 go build -a -o $@/syncbased v.io/syncbase/x/ref/services/syncbase/syncbased
 
-node_modules: package.json
+node_modules: package.json $(shell find $(V23_ROOT)/roadmap/javascript/syncbase)
 	npm prune
 	npm install
 	touch $@
@@ -26,8 +44,14 @@
 	rm -rf ./node_modules/{vanadium,syncbase}
 	cd "$(V23_ROOT)/release/javascript/core" && npm link
 	npm link vanadium
+	rm -rf ./node_modules/syncbase
 	cd "$(V23_ROOT)/roadmap/javascript/syncbase" && npm link
 	npm link syncbase
+# Delete syncbase's copy of the vanadium module. If we don't do this, then two
+# copies of vanadium will get bundled, and unfortunately vanadium contains some
+# singletons, which break if there is more than one copy of the module.
+# See https://github.com/vanadium/issues/issues/155
+	rm -rf ./node_modules/syncbase/node_modules/vanadium
 	touch node_modules
 
 public/bundle.min.js: browser/index.js $(shell find browser) node_modules
@@ -38,17 +62,15 @@
 endif
 
 .PHONY: build
-build: go/bin node_modules public/bundle.min.js
+build: bin node_modules public/bundle.min.js
 
 .PHONY: serve
-serve: export PATH := test:$(PATH)
 serve: build
-	node ./node_modules/vanadium/test/integration/runner.js --services=start-syncbased.sh -- \
 	npm start
 
 .PHONY: clean
 clean:
-	rm -rf go/bin node_modules public/bundle.min.js
+	rm -rf bin node_modules public/bundle.min.js
 
 .PHONY: lint
 lint:
diff --git a/README.md b/README.md
index 9c6b6f2..9ae3f96 100644
--- a/README.md
+++ b/README.md
@@ -10,3 +10,29 @@
 
     $V23_ROOT/release/go/bin/namespace glob -v23.namespace.root=V23_NAMESPACE -v23.credentials=V23_CREDENTIALS "test/*"
     $V23_ROOT/release/go/bin/vrpc signature -v23.namespace.root=V23_NAMESPACE -v23.credentials=V23_CREDENTIALS "test/syncbased/todos"
+
+## Notes
+
+- problem was that the extension defaults to prod mounttable and assumes
+  blessings minted by prod identity server, but local mount table and syncbased
+  run with local credentials and do not include dev.v.io in trusted roots.
+
+- one solution is to configure the extension with local identityd,
+  identitydBlessingUrl, and namespaceRoot, but this requires running Chrome as
+  follows and manually editing the Chrome extension options on each restart.
+
+  /Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --ignore-certificate-errors --user-data-dir=/tmp/foo
+
+- to avoid manually editing options, we could build the Chrome extension as part
+  of "make serve" - that's what our tests do. ew.
+
+- alternatively, we can include dev.v.io in our local mount table and
+  syncbased's trusted root sets, and have their "root dirs" allow access to
+  anyone. secure b/c these services are only accessible on localhost. for this
+  to work, we'd need to configure the webapp to talk to the local mount table.
+
+- even simpler, we could bypass mount table completely and have the webapp talk
+  directly to local syncbased. in addition, instead of overriding the trusted
+  root set, we can run the local syncbased with a dev.v.io blessing by using
+  seekblessings. (both here and above, all extension opts are left untouched,
+  and dev.v.io blessings are used.)
diff --git a/browser/collection.js b/browser/collection.js
index a819839..f237c29 100644
--- a/browser/collection.js
+++ b/browser/collection.js
@@ -1,3 +1,5 @@
+// Defines the Collection interface, a subset of the MongoDB collection API.
+
 'use strict';
 
 var EventEmitter = require('events').EventEmitter;
@@ -6,11 +8,11 @@
 inherits(Collection, EventEmitter);
 module.exports = Collection;
 
+// Collection interface. Collections emit 'change' events.
 function Collection() {
   EventEmitter.call(this);
 }
 
-// Collection interface, with most methods stubbed out.
 Collection.prototype.find = function(q, opts, cb) {
   throw new Error('not implemented');
 };
@@ -26,13 +28,3 @@
 Collection.prototype.update = function(q, opts, cb) {
   throw new Error('not implemented');
 };
-
-Collection.prototype.findOne = function(q, opts, cb) {
-  this.find(q, opts, function(err, all) {
-    if (err) return cb(err);
-    if (all.length > 0) {
-      return cb(null, all[0]);
-    }
-    return cb();
-  });
-};
diff --git a/browser/collection_dispatcher.js b/browser/collection_dispatcher.js
new file mode 100644
index 0000000..86be565
--- /dev/null
+++ b/browser/collection_dispatcher.js
@@ -0,0 +1,72 @@
+// Collection-based implementation of Dispatcher.
+
+'use strict';
+
+var _ = require('lodash');
+var inherits = require('util').inherits;
+
+var Collection = require('./collection');
+var Dispatcher = require('./dispatcher');
+
+inherits(CollectionDispatcher, Dispatcher);
+module.exports = CollectionDispatcher;
+
+function noop() {}
+
+function CollectionDispatcher(lists, todos) {
+  Dispatcher.call(this);
+  console.assert(lists instanceof Collection);
+  console.assert(todos instanceof Collection);
+  this.lists_ = lists;
+  this.todos_ = todos;
+}
+
+CollectionDispatcher.prototype.getLists = function(cb) {
+  this.lists_.find({}, {sort: {name: 1}}, cb);
+};
+
+CollectionDispatcher.prototype.getTodos = function(listId, cb) {
+  this.todos_.find({listId: listId}, {sort: {timestamp: 1}}, cb);
+};
+
+CollectionDispatcher.prototype.addList = function(list, cb) {
+  this.lists_.insert(list, this.maybeEmit_(cb));
+};
+
+CollectionDispatcher.prototype.editListName = function(listId, name, cb) {
+  this.lists_.update(listId, {$set: {name: name}}, this.maybeEmit_(cb));
+};
+
+CollectionDispatcher.prototype.addTodo = function(listId, todo, cb) {
+  todo = _.assign({}, todo, {listId: listId});
+  this.todos_.insert(todo, this.maybeEmit_(cb));
+};
+
+CollectionDispatcher.prototype.removeTodo = function(todoId, cb) {
+  this.todos_.remove(todoId, this.maybeEmit_(cb));
+};
+
+CollectionDispatcher.prototype.editTodoText = function(todoId, text, cb) {
+  this.todos_.update(todoId, {$set: {text: text}}, this.maybeEmit_(cb));
+};
+
+CollectionDispatcher.prototype.markTodoDone = function(todoId, done, cb) {
+  this.todos_.update(todoId, {$set: {done: done}}, this.maybeEmit_(cb));
+};
+
+CollectionDispatcher.prototype.addTag = function(todoId, tag, cb) {
+  this.todos_.update(todoId, {$addToSet: {tags: tag}}, this.maybeEmit_(cb));
+};
+
+CollectionDispatcher.prototype.removeTag = function(todoId, tag, cb) {
+  this.todos_.update(todoId, {$pull: {tags: tag}}, this.maybeEmit_(cb));
+};
+
+CollectionDispatcher.prototype.maybeEmit_ = function(cb) {
+  var that = this;
+  cb = cb || noop;
+  return function(err) {
+    cb.apply(null, arguments);
+    if (!err) that.emit('change');
+  };
+};
diff --git a/browser/defaults.js b/browser/defaults.js
index 001d722..4dd3ba7 100644
--- a/browser/defaults.js
+++ b/browser/defaults.js
@@ -3,10 +3,12 @@
 var async = require('async');
 var syncbase = require('syncbase');
 
-var Memstore = require('./memstore');
-var Syncbase = require('./syncbase');
+var CollectionDispatcher = require('./collection_dispatcher');
+var MemCollection = require('./mem_collection');
+var SyncbaseDispatcher = require('./syncbase_dispatcher');
 
-var SYNCBASE_NAME = 'test/syncbased';
+//var SYNCBASE_NAME = 'test/syncbased';
+var SYNCBASE_NAME = '/localhost:8200';
 
 // Copied from meteor/todos/server/bootstrap.js.
 var data = [
@@ -46,85 +48,70 @@
   }
 ];
 
-function initData(lists, todos, cb) {
+function initData(disp, cb) {
   var timestamp = Date.now();
   async.each(data, function(list, cb) {
-    lists.insert({name: list.name}, function(err, listId) {
+    disp.addList({name: list.name}, function(err, listId) {
       if (err) return cb(err);
       async.each(list.contents, function(info, cb) {
         timestamp += 1;  // ensure unique timestamp
-        todos.insert({
-          listId: listId,
+        disp.addTodo(listId, {
           text: info[0],
+          tags: info.slice(1),
           done: false,
-          timestamp: timestamp,
-          tags: info.slice(1)
+          timestamp: timestamp
         }, cb);
       }, cb);
     });
   }, cb);
 }
 
-function appExists(ctx, service, name, cb) {
-  service.listApps(ctx, function(err, names) {
+function newCtx(rt, timeout) {
+  timeout = timeout || 5000;
+  return rt.getContext().withTimeout(timeout);
+}
+
+function appExists(rt, service, name, cb) {
+  service.listApps(newCtx(rt), function(err, names) {
     if (err) return cb(err);
     return cb(null, names.indexOf(name) >= 0);
   });
 }
 
-exports.initCollections = function(ctx, engine, cb) {
-  function doInitData(lists, todos, cb) {
-    initData(lists, todos, function(err) {
-      if (err) return cb(err);
-      return cb(null, {
-        lists: lists,
-        todos: todos
-      });
-    });
-  }
-
-  switch (engine) {
-  case 'syncbase':
+exports.initDispatcher = function(rt, engine, cb) {
+  if (engine === 'syncbase') {
     var service = syncbase.newService(SYNCBASE_NAME);
-    appExists(ctx, service, 'todos', function(err, exists) {
+    appExists(rt, service, 'todos', function(err, exists) {
       if (err) return cb(err);
       var app = service.app('todos'), db = app.noSqlDatabase('db');
-      var lists = new Syncbase(db, 'lists');
-      var todos = new Syncbase(db, 'todos');
+      var disp = new SyncbaseDispatcher(rt, db);
       if (exists) {
         console.log('app exists; assuming everything has been initialized');
-        return cb(null, {
-          lists: lists,
-          todos: todos
-        });
+        return cb(null, disp);
       }
       console.log('app does not exist; initializing everything');
-      app.create(ctx, {}, function(err) {
-        console.log('app.create done');
-        // TODO(sadovsky): This fails with "No usable servers found". Chat with
-        // Nick to determine optimal setup for development and debugging.
+      app.create(newCtx(rt), {}, function(err) {
         if (err) return cb(err);
-        var db = app.noSqlDatabase('db');
-        db.create(ctx, {}, function(err) {
+        db.create(newCtx(rt), {}, function(err) {
           if (err) return cb(err);
-          async.each(['lists', 'todos'], function(name, cb) {
-            db.createTable(ctx, name, {}, cb);
-          }, function(err) {
+          db.createTable(newCtx(rt), 'tb', {}, function(err) {
             if (err) return cb(err);
-            var lists = new Syncbase(db, 'lists');
-            var todos = new Syncbase(db, 'todos');
-            doInitData(lists, todos, cb);
+            initData(disp, function(err) {
+              if (err) return cb(err);
+              return cb(null, disp);
+            });
           });
         });
       });
     });
-    break;
-  case 'memstore':
-    var lists = new Memstore('lists');
-    var todos = new Memstore('todos');
-    doInitData(lists, todos, cb);
-    break;
-  default:
+  } else if (engine === 'memstore') {
+    var lists = new MemCollection('lists'), todos = new MemCollection('todos');
+    var disp = new CollectionDispatcher(lists, todos);
+    initData(disp, function(err) {
+      if (err) return cb(err);
+      return cb(null, disp);
+    });
+  } else {
     throw new Error('unknown engine: ' + engine);
   }
 };
diff --git a/browser/dispatcher.js b/browser/dispatcher.js
index a9d6f15..3c109f1 100644
--- a/browser/dispatcher.js
+++ b/browser/dispatcher.js
@@ -1,49 +1,58 @@
-// Note, our Dispatcher combines the following React Flux concepts: Actions,
-// Dispatcher, and Stores.
+// Defines the Dispatcher interface. Loosely inspired by React Flux.
 
 'use strict';
 
+var EventEmitter = require('events').EventEmitter;
+var inherits = require('util').inherits;
+
+inherits(Dispatcher, EventEmitter);
 module.exports = Dispatcher;
 
-function Dispatcher(lists, todos) {
-  this.lists_ = lists;
-  this.todos_ = todos;
+// Dispatcher interface. Dispatchers emit 'change' events.
+function Dispatcher() {
+  EventEmitter.call(this);
 }
 
-function noop() {}
+// Returns lists with _id.
+Dispatcher.prototype.getLists = function(cb) {
+  throw new Error('not implemented');
+};
 
-// Note, we pass noop as the callback everywhere since our app handles all
-// updates by watching for changes.
-// TODO(sadovsky): Pass a callback and handle errors.
-Dispatcher.prototype = {
-  addList: function(name) {
-    return this.lists_.insert({name: name}, noop);
-  },
-  editListName: function(listId, name) {
-    this.lists_.update(listId, {$set: {name: name}}, noop);
-  },
-  addTodo: function(listId, text, tags) {
-    return this.todos_.insert({
-      listId: listId,
-      text: text,
-      done: false,
-      timestamp: (new Date()).getTime(),
-      tags: tags
-    }, noop);
-  },
-  removeTodo: function(todoId) {
-    this.todos_.remove(todoId, noop);
-  },
-  editTodoText: function(todoId, text) {
-    this.todos_.update(todoId, {$set: {text: text}}, noop);
-  },
-  markTodoDone: function(todoId, done) {
-    this.todos_.update(todoId, {$set: {done: done}}, noop);
-  },
-  addTag: function(todoId, tag) {
-    this.todos_.update(todoId, {$addToSet: {tags: tag}}, noop);
-  },
-  removeTag: function(todoId, tag) {
-    this.todos_.update(todoId, {$pull: {tags: tag}}, noop);
-  }
+// Returns todos with _id and tags.
+Dispatcher.prototype.getTodos = function(listId, cb) {
+  throw new Error('not implemented');
+};
+
+// The given list must not have _id.
+Dispatcher.prototype.addList = function(list, cb) {
+  throw new Error('not implemented');
+};
+
+Dispatcher.prototype.editListName = function(listId, name, cb) {
+  throw new Error('not implemented');
+};
+
+// The given todo must not have _id, but may have tags.
+Dispatcher.prototype.addTodo = function(listId, todo, cb) {
+  throw new Error('not implemented');
+};
+
+Dispatcher.prototype.removeTodo = function(todoId, cb) {
+  throw new Error('not implemented');
+};
+
+Dispatcher.prototype.editTodoText = function(todoId, text, cb) {
+  throw new Error('not implemented');
+};
+
+Dispatcher.prototype.markTodoDone = function(todoId, done, cb) {
+  throw new Error('not implemented');
+};
+
+Dispatcher.prototype.addTag = function(todoId, tag, cb) {
+  throw new Error('not implemented');
+};
+
+Dispatcher.prototype.removeTag = function(todoId, tag, cb) {
+  throw new Error('not implemented');
 };
diff --git a/browser/index.js b/browser/index.js
index 1f04fcd..eedafb4 100644
--- a/browser/index.js
+++ b/browser/index.js
@@ -11,14 +11,12 @@
 var vanadium = require('vanadium');
 
 var defaults = require('./defaults');
-var Dispatcher = require('./dispatcher');
 var h = require('./util').h;
 
 ////////////////////////////////////////
 // Global state
 
-var cLists, cTodos;  // collections
-var disp;  // dispatcher
+var disp;  // type Dispatcher
 
 ////////////////////////////////////////
 // Helpers
@@ -126,6 +124,8 @@
             ev.target.parentNode.style.opacity = 0;
             // Wait for CSS animation to finish.
             window.setTimeout(function() {
+              // TODO(sadovsky): If no other todos have the removed tag, set
+              // tagFilter to null.
               disp.removeTag(that.props.todoId, tag);
             }, 300);
           }
@@ -232,8 +232,12 @@
         placeholder: 'New item'
       }, okCancelEvents({
         ok: function(value, ev) {
-          var tags = tagFilter ? [tagFilter] : [];
-          disp.addTodo(that.props.listId, value, tags);
+          disp.addTodo(that.props.listId, {
+            text: value,
+            tags: tagFilter ? [tagFilter] : [],
+            done: false,
+            timestamp: Date.now()
+          });
           ev.target.value = '';
         }
       })))));
@@ -313,8 +317,9 @@
         placeholder: 'New list'
       }, okCancelEvents({
         ok: function(value, ev) {
-          var id = disp.addList(value);
-          that.props.setListId(id);
+          disp.addList({name: value}, function(err, listId) {
+            that.props.setListId(listId);
+          });
           ev.target.value = '';
         }
       })))));
@@ -333,14 +338,14 @@
       tagFilter: null  // current tag
     };
   },
-  fetchLists_: function(cb) {
-    return cLists.find({}, {sort: {name: 1}}, cb);
+  getLists_: function(cb) {
+    disp.getLists(cb);
   },
-  fetchTodos_: function(listId, cb) {
+  getTodos_: function(listId, cb) {
     if (listId === null) {
       return cb();
     }
-    return cTodos.find({listId: listId}, {sort: {timestamp: 1}}, cb);
+    disp.getTodos(listId, cb);
   },
   updateURL: function() {
     var router = this.props.router, listId = this.state.listId;
@@ -349,26 +354,27 @@
   componentDidMount: function() {
     var that = this;
 
-    cLists.on('change', function() {
-      that.fetchLists_(function(err, lists) {
+    // TODO(sadovsky): Only update what's needed based on what changed.
+    disp.on('change', function() {
+      that.getLists_(function(err, lists) {
         if (err) throw err;
-        that.setState({lists: lists});
-      });
-    });
-    cTodos.on('change', function() {
-      that.fetchTodos_(that.state.listId, function(err, todos) {
-        if (err) throw err;
-        that.setState({todos: todos});
+        that.getTodos_(that.state.listId, function(err, todos) {
+          if (err) throw err;
+          that.setState({
+            lists: lists,
+            todos: todos
+          });
+        });
       });
     });
 
-    that.fetchLists_(function(err, lists) {
+    that.getLists_(function(err, lists) {
       if (err) throw err;
       var listId = that.state.listId;
       if (listId === null && lists.length > 0) {
         listId = lists[0]._id;
       }
-      that.fetchTodos_(listId, function(err, todos) {
+      that.getTodos_(listId, function(err, todos) {
         if (err) throw err;
         that.setState({
           lists: lists,
@@ -402,7 +408,8 @@
         listId: this.state.listId,
         setListId: function(listId) {
           if (listId !== that.state.listId) {
-            that.fetchTodos_(listId, function(err, todos) {
+            // TODO(sadovsky): Get todos as a separate async step?
+            that.getTodos_(listId, function(err, todos) {
               if (err) throw err;
               that.setState({
                 todos: todos,
@@ -430,11 +437,9 @@
 vanadium.init(vanadiumConfig, function(err, rt) {
   if (err) throw err;
   var engine = u.query.engine || 'memstore';
-  defaults.initCollections(rt.getContext(), engine, function(err, cxs) {
+  defaults.initDispatcher(rt, engine, function(err, resDisp) {
     if (err) throw err;
-    cLists = cxs.lists;
-    cTodos = cxs.todos;
-    disp = new Dispatcher(cLists, cTodos);
+    disp = resDisp;
 
     var Router = Backbone.Router.extend({
       routes: {
diff --git a/browser/memstore.js b/browser/mem_collection.js
similarity index 76%
rename from browser/memstore.js
rename to browser/mem_collection.js
index 8be3a82..51b8f6f 100644
--- a/browser/memstore.js
+++ b/browser/mem_collection.js
@@ -1,4 +1,5 @@
-// TODO(sadovsky): Use minimongo?
+// In-memory implementation of Collection.
+// TODO(sadovsky): Replace with nedb NPM module.
 
 'use strict';
 
@@ -7,17 +8,20 @@
 
 var Collection = require('./collection');
 
-inherits(Memstore, Collection);
-module.exports = Memstore;
+inherits(MemCollection, Collection);
+module.exports = MemCollection;
 
-function Memstore(name) {
+function MemCollection(name) {
   Collection.call(this);
   this.name_ = name;
   this.vals_ = [];
 }
 
-Memstore.prototype.find = function(q, opts, cb) {
+function noop() {}
+
+MemCollection.prototype.find = function(q, opts, cb) {
   var that = this;
+  cb = cb || noop;
   q = this.normalize_(q);
   var res = _.filter(this.vals_, function(v) {
     return that.matches_(v, q);
@@ -33,16 +37,18 @@
   return cb(null, _.cloneDeep(res));
 };
 
-Memstore.prototype.insert = function(v, cb) {
-  console.assert(!_.has(v, '_id'));
+MemCollection.prototype.insert = function(v, cb) {
+  cb = cb || noop;
+  console.assert(!v._id);
   v = _.assign({}, v, {_id: this.vals_.length});
   this.vals_.push(v);
   this.emit('change');
   return cb(null, v._id);
 };
 
-Memstore.prototype.remove = function(q, cb) {
+MemCollection.prototype.remove = function(q, cb) {
   var that = this;
+  cb = cb || noop;
   q = this.normalize_(q);
   this.vals_ = _.filter(this.vals_, function(v) {
     return !that.matches_(v, q);
@@ -51,8 +57,9 @@
   return cb();
 };
 
-Memstore.prototype.update = function(q, opts, cb) {
+MemCollection.prototype.update = function(q, opts, cb) {
   var that = this;
+  cb = cb || noop;
   q = this.normalize_(q);
   var vals = _.filter(this.vals_, function(v) {
     return that.matches_(v, q);
@@ -86,14 +93,14 @@
   return cb();
 };
 
-Memstore.prototype.normalize_ = function(q) {
+MemCollection.prototype.normalize_ = function(q) {
   if (_.isObject(q)) {
     return q;
   }
   return {_id: q};
 };
 
-Memstore.prototype.matches_ = function(v, q) {
+MemCollection.prototype.matches_ = function(v, q) {
   var keys = _.keys(q);
   for (var i = 0; i < keys.length; i++) {
     var key = keys[i];
diff --git a/browser/syncbase.js b/browser/syncbase.js
deleted file mode 100644
index f82a401..0000000
--- a/browser/syncbase.js
+++ /dev/null
@@ -1,36 +0,0 @@
-// Syncbase wrapper that implements the Collection API.
-
-// TODO(sadovsky): Implement.
-
-'use strict';
-
-var inherits = require('util').inherits;
-
-var Collection = require('./collection');
-
-inherits(Syncbase, Collection);
-module.exports = Syncbase;
-
-// TODO(sadovsky): Watch store for change events. (Necessary if we want to
-// immediately display synced data.)
-
-function Syncbase(db, tableName) {
-  Collection.call(this);
-  this.table_ = db.table(tableName);
-}
-
-Syncbase.prototype.find = function(q, opts, cb) {
-  throw new Error('not implemented');
-};
-
-Syncbase.prototype.insert = function(v, cb) {
-  throw new Error('not implemented');
-};
-
-Syncbase.prototype.remove = function(q, cb) {
-  throw new Error('not implemented');
-};
-
-Syncbase.prototype.update = function(q, opts, cb) {
-  throw new Error('not implemented');
-};
diff --git a/browser/syncbase_dispatcher.js b/browser/syncbase_dispatcher.js
new file mode 100644
index 0000000..6914caa
--- /dev/null
+++ b/browser/syncbase_dispatcher.js
@@ -0,0 +1,235 @@
+// Syncbase-based implementation of Dispatcher.
+//
+// Schema design doc (a bit outdated):
+// https://docs.google.com/document/d/1GtBk75QmjSorUW6T6BATCoiS_LTqOrGksgqjqJ1Hiow/edit#
+//
+// NOTE: Currently, list and todo order are not preserved. We should make the
+// app always order these lexicographically.
+
+'use strict';
+
+var _ = require('lodash');
+var async = require('async');
+var inherits = require('util').inherits;
+var nodeUuid = require('node-uuid');
+
+var syncbase = require('syncbase');
+var nosql = syncbase.nosql;
+
+var Dispatcher = require('./Dispatcher');
+
+inherits(SyncbaseDispatcher, Dispatcher);
+module.exports = SyncbaseDispatcher;
+
+function SyncbaseDispatcher(rt, db) {
+  Dispatcher.call(this);
+  this.rt_ = rt;
+  this.db_ = db;
+  this.tb_ = db.table('tb');
+}
+
+////////////////////////////////////////
+// Helpers
+
+function noop() {}
+
+var SEP = '.';  // separator for key parts
+
+function join() {
+  // TODO(sadovsky): Switch to using naming.join() once Syncbase allows slashes
+  // in row keys.
+  var args = Array.prototype.slice.call(arguments);
+  return args.join(SEP);
+}
+
+function uuid() {
+  return nodeUuid.v4();
+}
+
+function newListKey() {
+  return uuid();
+}
+
+function newTodoKey(listId) {
+  return join(listId, 'todos', uuid());
+}
+
+function tagKey(todoId, tag) {
+  return join(todoId, 'tags', tag);
+}
+
+function marshal(x) {
+  return JSON.stringify(x);
+}
+
+function unmarshal(x) {
+  return JSON.parse(x);
+}
+
+////////////////////////////////////////
+// SyncbaseDispatcher impl
+
+// TODO(sadovsky): Switch to storing VDL values (instead of JSON) and use a
+// query to get all values of a particular type.
+SyncbaseDispatcher.prototype.getLists = function(cb) {
+  this.getRows_(function(err, rows) {
+    if (err) return cb(err);
+    var lists = [];
+    _.forEach(rows, function(row) {
+      if (row.key.indexOf(SEP) >= 0) {
+        return;
+      }
+      lists.push(_.assign({}, unmarshal(row.value), {_id: row.key}));
+    });
+    return cb(null, lists);
+  });
+};
+
+SyncbaseDispatcher.prototype.getTodos = function(listId, cb) {
+  this.getRows_(function(err, rows) {
+    if (err) return cb(err);
+    var todos = [];
+    var todo = {};
+    _.forEach(rows, function(row) {
+      var parts = row.key.split(SEP);
+      if (parts.length < 2 || parts[0] !== listId) {
+        return;
+      } else if (parts.length === 3) {  // next todo
+        if (todo._id) {
+          todos.push(todo);
+        }
+        todo = _.assign({}, unmarshal(row.value), {_id: row.key});
+      } else if (parts.length === 5) {  // tag for current todo
+        if (!todo.tags) {
+          todo.tags = [];
+        }
+        todo.tags.push(parts[4]);  // push tag name
+      } else {
+        throw new Error('bad key: ' + row.key);
+      }
+    });
+    return cb(null, todos);
+  });
+};
+
+SyncbaseDispatcher.prototype.addList = function(list, cb) {
+  console.assert(!list._id);
+  var listId = newListKey();
+  var v = marshal(list);
+  this.tb_.put(this.newCtx_(), listId, v, this.maybeEmit_(function(err) {
+    if (err) return cb(err);
+    return cb(null, listId);
+  }));
+};
+
+SyncbaseDispatcher.prototype.editListName = function(listId, name, cb) {
+  this.update_(listId, function(list) {
+    return _.assign(list, {name: name});
+  }, cb);
+};
+
+SyncbaseDispatcher.prototype.addTodo = function(listId, todo, cb) {
+  var that = this;
+  console.assert(!todo._id);
+  var tags = todo.tags;
+  delete todo.tags;
+  var todoId = newTodoKey(listId);
+  var v = marshal(todo);
+  // Write todo and tags in a batch.
+  var opts = new nosql.BatchOptions();
+  nosql.runInBatch(this.newCtx_(), this.db_, opts, function(db, cb) {
+    // NOTE: Dealing with tables is awkward given that batches and syncgroups
+    // are database-level. Maybe we should just get rid of tables. Doing so
+    // would solve other problems as well, e.g. the API inconsistency for
+    // creating databases vs. tables.
+    var tb = db.table('tb');
+    tb.put(that.newCtx_(), todoId, v, function(err) {
+      if (err) return cb(err);
+      async.each(tags, function(tag, cb) {
+        that.addTagInternal_(tb, todoId, tag, cb);
+      }, cb);
+    });
+  }, this.maybeEmit_(cb));
+};
+
+SyncbaseDispatcher.prototype.removeTodo = function(todoId, cb) {
+  this.tb_.row(todoId).delete(this.newCtx_(), this.maybeEmit_(cb));
+};
+
+SyncbaseDispatcher.prototype.editTodoText = function(todoId, text, cb) {
+  this.update_(todoId, function(todo) {
+    return _.assign(todo, {text: text});
+  }, cb);
+};
+
+SyncbaseDispatcher.prototype.markTodoDone = function(todoId, done, cb) {
+  this.update_(todoId, function(todo) {
+    return _.assign(todo, {done: done});
+  }, cb);
+};
+
+SyncbaseDispatcher.prototype.addTag = function(todoId, tag, cb) {
+  this.addTagInternal_(this.tb_, todoId, tag, this.maybeEmit_(cb));
+};
+
+SyncbaseDispatcher.prototype.removeTag = function(todoId, tag, cb) {
+  // NOTE: Table.delete is awkward (it takes a range), so instead we use
+  // Row.delete. It would be nice for Table.delete to operate on a single row
+  // and have a separate Table.deleteRowRange.
+  this.tb_.row(tagKey(todoId, tag)).delete(this.newCtx_(), this.maybeEmit_(cb));
+};
+
+// TODO(sadovsky): Watch for changes on Syncbase itself so that we can detect
+// when data arrives via sync, and drop this method.
+SyncbaseDispatcher.prototype.maybeEmit_ = function(cb) {
+  var that = this;
+  cb = cb || noop;
+  return function(err) {
+    cb.apply(null, arguments);
+    if (!err) that.emit('change');
+  };
+};
+
+// Returns a new Vanadium context object with a timeout.
+SyncbaseDispatcher.prototype.newCtx_ = function(timeout) {
+  timeout = timeout || 5000;
+  return this.rt_.getContext().withTimeout(timeout);
+};
+
+// Writes the given tag into the given table.
+SyncbaseDispatcher.prototype.addTagInternal_ = function(tb, todoId, tag, cb) {
+  // NOTE: Syncbase currently disallows whitespace in keys, so as a quick hack
+  // we drop all whitespace before storing tags.
+  tag = tag.replace(/\s+/g, '');
+  tb.put(this.newCtx_(), tagKey(todoId, tag), null, cb);
+};
+
+// Returns all rows in the table.
+SyncbaseDispatcher.prototype.getRows_ = function(cb) {
+  var rows = [], streamErr = null;
+  var range = nosql.rowrange.prefix('');
+  this.tb_.scan(this.newCtx_(), range, function(err) {
+    if (err) return cb(err);
+    if (streamErr) return cb(streamErr);
+    cb(null, rows);
+  }).on('data', function(row) {
+    rows.push(row);
+  }).on('error', function(err) {
+    streamErr = streamErr || err.error;
+  });
+};
+
+// Performs a read-modify-write on key, applying updateFn to the value.
+// Takes care of value marshalling and unmarshalling.
+SyncbaseDispatcher.prototype.update_ = function(key, updateFn, cb) {
+  var that = this;
+  var opts = new nosql.BatchOptions();
+  nosql.runInBatch(this.newCtx_(), this.db_, opts, function(db, cb) {
+    var tb = db.table('tb');
+    tb.get(that.newCtx_(), key, function(err, value) {
+      if (err) return cb(err);
+      var newValue = marshal(updateFn(unmarshal(value)));
+      tb.put(that.newCtx_(), key, newValue, cb);
+    });
+  }, this.maybeEmit_(cb));
+};
diff --git a/package.json b/package.json
index 4e22922..a48c127 100644
--- a/package.json
+++ b/package.json
@@ -16,12 +16,14 @@
     "async": "^1.2.1",
     "express": "^4.12.4",
     "lodash": "^3.9.3",
+    "node-uuid": "^1.4.3",
     "react": "^0.13.3"
   },
   "devDependencies": {
     "browserify": "^10.2.3",
     "browserify-shim": "^3.8.8",
     "jshint": "^2.8.0",
-    "minifyify": "^7.0.0"
+    "exorcist": "~0.4.0",
+    "uglifyify": "~3.0.1"
   }
 }
diff --git a/start_syncbased.sh b/start_syncbased.sh
new file mode 100755
index 0000000..c806ac7
--- /dev/null
+++ b/start_syncbased.sh
@@ -0,0 +1,10 @@
+#!/bin/bash
+# Copyright 2015 The Vanadium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file.
+
+# Expects credentials in /tmp/creds, generated as follows:
+# make build
+# ./bin/principal seekblessings --v23.credentials tmp/creds
+
+./bin/syncbased --root-dir=tmp/sbroot --v23.tcp.address=localhost:8200 --v23.credentials=tmp/creds --v=3 --alsologtostderr=true --v23.permissions.literal='{"Admin":{"In":["..."]},"Write":{"In":["..."]},"Read":{"In":["..."]},"Resolve":{"In":["..."]},"Debug":{"In":["..."]}}'
diff --git a/test/start-syncbased.sh b/test/start-syncbased.sh
deleted file mode 100755
index d6c2e11..0000000
--- a/test/start-syncbased.sh
+++ /dev/null
@@ -1,13 +0,0 @@
-#!/bin/bash
-# Copyright 2015 The Vanadium Authors. All rights reserved.
-# Use of this source code is governed by a BSD-style
-# license that can be found in the LICENSE file.
-
-# Start syncbased and mount in the mounttable.
-
-# TODO(nlacasse): This file is needed because the javascript service-runner
-# does not allow flags or arguments to the executables it starts.  We should
-# fix service-runner to allow flags/arguments, and then have it start syncbased
-# directly with the appropriate flags.  Then we can delete this file.
-
-syncbased -v=1 --name test/syncbased --engine memstore