syncbase todosapp: fill out README; tweaks to setup

Change-Id: If41e0f8bbfdd7738349dd2e615ff2f0e00965af5
diff --git a/Makefile b/Makefile
index 46ff0f9..dcfa1f1 100644
--- a/Makefile
+++ b/Makefile
@@ -4,13 +4,13 @@
 # Default browserify options: use sourcemaps.
 BROWSERIFY_OPTS := --debug
 # Names that should not be mangled by minification.
-RESERVED_NAMES := "context,ctx,callback,cb,$$stream,serverCall"
+RESERVED_NAMES := 'context,ctx,callback,cb,$$stream,serverCall'
 # Don't mangle RESERVED_NAMES, and screw ie8.
-MANGLE_OPTS := --mangle [--except $(RESERVED_NAMES) --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]
+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
@@ -35,7 +35,7 @@
 define BROWSERIFY_MIN
 	mkdir -p $(dir $2)
 	$(INCREASE_FILE_DESC); \
-	browserify $1 $(BROWSERIFY_OPTS) --g [uglifyify $(MANGLE_OPTS) $(COMPRESS_OPTS)] | exorcist $2.map > $2
+	browserify $1 $(BROWSERIFY_OPTS) --g [ uglifyify $(MANGLE_OPTS) $(COMPRESS_OPTS) ] | exorcist $2.map > $2
 endef
 
 .DELETE_ON_ERROR:
diff --git a/README.md b/README.md
index 3011e3e..68ba77f 100644
--- a/README.md
+++ b/README.md
@@ -1,16 +1,94 @@
-# Todos
+# Todos app
 
-Todos is an example app that demonstrates Syncbase.
+Todos is an example app that demonstrates use of [Syncbase][syncbase].
 
 ## Running the web application
 
-    make serve
+The commands below assume that the current working directory is
+`$V23_ROOT/experimental/projects/todosapp`.
+
+First, build all necessary binaries.
+
+    DEBUG=1 make build
+
+Next, if you haven't already, generate credentials to use for running the local
+Syncbase daemon. When prompted, specify blessing extension "syncbase". Note, the
+value of `--v23.credentials` should correspond to the `$TMPDIR` value specified
+when running `start_syncbased.sh`.
+
+    ./bin/principal seekblessings --v23.credentials tmp/creds
+
+Next, start a local Syncbase daemon (in another terminal). Note, this script
+expects credentials in `$TMPDIR/creds`, and configures Syncbase to persist data
+under root directory `$TMPDIR/syncbase`.
+
+    TMPDIR=tmp ./start_syncbased.sh
+
+Finally, start the web app.
+
+    DEBUG=1 make serve
+
+Visit `http://localhost:4000` in your browser to access the app.
+
+### Using Syncbase
+
+By default, the web app will use an in-memory (in-browser-tab) local storage
+engine, and will not talk to Syncbase at all. To configure the app to talk to
+Syncbase, add `d=syncbase` to the url query params, or simply click the storage
+engine indicator in the upper right corner to toggle it.
+
+When using Syncbase, by default the app attempts to contact the Syncbase service
+using the Vanadium object name `/localhost:8200`. To specify a different name,
+add `n=<name>` to the url query params.
+
+Beware that `start_syncbased.sh` starts Syncbase with completely open ACLs. This
+is safe if Syncbase is only accessible locally (the default), but more dangerous
+if this Syncbase instance is configured to be accessible via a global mount
+table.
+
+## Design and implementation
+
+Todos is implemented as a single-page JavaScript web application that
+communicates with a local Syncbase daemon through the
+[Vanadium Chrome extension][crx]. The app UI is built using HTML and CSS, using
+React as a model-view framework.
+
+The Syncbase data layout and conflict resolution scheme for this app are
+[described here][design]. For now, when an item is deleted, any sub-items that
+were added concurrently (on some other device) are orphaned. Eventually, we'll
+GC orphaned records; for now, we don't bother. This orphaning-based approach
+enables us to use simple last-one-wins conflict resolution for all records
+stored in Syncbase.
+
+At startup, the web app checks whether its backing store (e.g. Syncbase) is
+empty; if so, it writes some todo lists to the store (see
+`browser/defaults.js`). Next, the app proceeds to render the UI. To do so, it
+scans the store and sets up in-memory data structures representing the user's
+todo lists, then draws the UI (using React) based on the state of these
+in-memory data structures.
+
+When a user performs a mutation through the UI, the app issues a corresponding
+method call against its dispatcher (see `browser/dispatcher.js`), which ends up
+writing to the backing store and emitting a `'change'` event. The web app
+listens for `'change'` events; when one is received, it re-reads any pertinent
+state from the backing store (again, via the dispatcher interface), updates its
+in-memory data structures, and redraws the UI.
+
+When changes are received via Syncbase sync, the dispatcher discovers these
+changes (currently via polling; soon, via watch) and emits a `'change'` event,
+triggering the same redraw procedure as described above.
 
 ## Resources for debugging
 
-    https://sites.google.com/a/google.com/v-prod/
-    https://sites.google.com/a/google.com/v-prod/vanadium-services/how-to
+- https://sites.google.com/a/google.com/v-prod/
+- https://sites.google.com/a/google.com/v-prod/vanadium-services/how-to
+
+### Commands
 
     $V23_ROOT/release/go/bin/namespace -v23.credentials=V23_CREDENTIALS -v23.namespace.root=V23_NAMESPACE glob "test/..."
     $V23_ROOT/release/go/bin/vrpc -v23.credentials=V23_CREDENTIALS -v23.namespace.root=V23_NAMESPACE signature "test/syncbase"
     $V23_ROOT/release/go/bin/debug -v23.credentials=V23_CREDENTIALS -v23.namespace.root=V23_NAMESPACE stats read /localhost:8200/__debug/stats/rpc/server/routing-id/393ccca2ee7979d026374e76b2846e0b/methods/Delete/latency-ms
+
+[syncbase]: https://docs.google.com/document/d/12wS_IEPf8HTE7598fcmlN-Y692OWMSneoe2tvyBEpi0/edit#
+[crx]: https://v.io/tools/vanadium-chrome-extension.html
+[design]: https://docs.google.com/document/d/1GtBk75QmjSorUW6T6BATCoiS_LTqOrGksgqjqJ1Hiow/edit
diff --git a/browser/defaults.js b/browser/defaults.js
index b97825d..8a3abe4 100644
--- a/browser/defaults.js
+++ b/browser/defaults.js
@@ -7,9 +7,6 @@
 var MemCollection = require('./mem_collection');
 var SyncbaseDispatcher = require('./syncbase_dispatcher');
 
-//var SYNCBASE_NAME = 'test/syncbased';
-var SYNCBASE_NAME = '/localhost:8200';
-
 // Copied from meteor/todos/server/bootstrap.js.
 var data = [
   {name: 'Meteor Principles',
@@ -78,8 +75,8 @@
   });
 }
 
-exports.initSyncbaseDispatcher = function(rt, cb) {
-  var service = syncbase.newService(SYNCBASE_NAME);
+exports.initSyncbaseDispatcher = function(rt, name, cb) {
+  var service = syncbase.newService(name);
   // TODO(sadovsky): Instead of appExists, simply check for ErrExist in the
   // app.create response.
   appExists(rt, service, 'todos', function(err, exists) {
diff --git a/browser/index.js b/browser/index.js
index 0dcacb3..4493631 100644
--- a/browser/index.js
+++ b/browser/index.js
@@ -15,6 +15,12 @@
 var h = require('./util').h;
 
 ////////////////////////////////////////
+// Constants
+
+var DISP_TYPE_COLLECTION = 'collection';
+var DISP_TYPE_SYNCBASE = 'syncbase';
+
+////////////////////////////////////////
 // Global state
 
 var disp;  // type Dispatcher
@@ -332,7 +338,12 @@
 
 var DispType = React.createFactory(React.createClass({
   render: function() {
-    return h('div.disp-type.' + this.props.dispType, this.props.dispType);
+    var that = this;
+    return h('div.disp-type.' + this.props.dispType, {
+      onClick: function() {
+        that.props.toggleDispType();
+      }
+    }, this.props.dispType);
   }
 }));
 
@@ -366,6 +377,7 @@
   updateURL: function() {
     var listId = this.state.listId;
     var pathname = !listId ? '/' : '/lists/' + listId;
+    // Note, this doesn't trigger a re-render; it's purely visual.
     window.history.replaceState({}, '', pathname + window.location.search);
   },
   componentDidMount: function() {
@@ -418,7 +430,17 @@
   render: function() {
     var that = this;
     return h('div', [
-      DispType({dispType: this.props.dispType}),
+      DispType({
+        dispType: this.props.dispType,
+        toggleDispType: function() {
+          var newDispType = DISP_TYPE_SYNCBASE;
+          if (that.props.dispType === DISP_TYPE_SYNCBASE) {
+            newDispType = DISP_TYPE_COLLECTION;
+          }
+          // TODO(sadovsky): Retain other query params, namely 'n'.
+          window.location.href = '/?d=' + newDispType;
+        }
+      }),
       h('div#top-tag-filter', TagFilter({
         todos: this.state.todos,
         tagFilter: this.state.tagFilter,
@@ -472,7 +494,7 @@
   rc = React.render(Page(props), document.getElementById('page'));
 }
 
-function initDispatcher(dispType, cb) {
+function initDispatcher(dispType, syncbaseName, cb) {
   if (dispType === 'collection') {
     defaults.initCollectionDispatcher(cb);
   } else if (dispType === 'syncbase') {
@@ -483,7 +505,7 @@
     };
     vanadium.init(vanadiumConfig, function(err, rt) {
       if (err) return cb(err);
-      defaults.initSyncbaseDispatcher(rt, cb);
+      defaults.initSyncbaseDispatcher(rt, syncbaseName, cb);
     });
   } else {
     process.nextTick(function() {
@@ -495,11 +517,13 @@
 function main(ctx) {
   console.assert(!rc);
   var dispType = u.query.d || 'collection';
+  var syncbaseName = u.query.n || '/localhost:8200';
   var props = {
     initialListId: ctx.params.listId,
-    dispType: dispType
+    dispType: dispType,
+    syncbaseName: syncbaseName
   };
-  initDispatcher(dispType, function(err, resDisp) {
+  initDispatcher(dispType, syncbaseName, function(err, resDisp) {
     if (err) throw err;
     disp = resDisp;
     defaults.initData(disp, function(err) {
diff --git a/browser/syncbase_dispatcher.js b/browser/syncbase_dispatcher.js
index 1090c81..513c171 100644
--- a/browser/syncbase_dispatcher.js
+++ b/browser/syncbase_dispatcher.js
@@ -16,7 +16,7 @@
 var syncbase = require('syncbase');
 var nosql = syncbase.nosql;
 
-var Dispatcher = require('./Dispatcher');
+var Dispatcher = require('./dispatcher');
 
 inherits(SyncbaseDispatcher, Dispatcher);
 module.exports = SyncbaseDispatcher;
diff --git a/public/extras.css b/public/extras.css
index eaabbdd..57fac51 100644
--- a/public/extras.css
+++ b/public/extras.css
@@ -2,15 +2,17 @@
   position: fixed;
   top: 0;
   right: 0;
+  padding: 4px 8px;
+  cursor: pointer;
   color: white;
-  padding: 0 5px;
+  font-weight: bold;
   z-index: 1;
 }
 
+/* https://www.google.com/design/spec/style/color.html */
 .disp-type.collection {
-  background-color: red;
+  background-color: #388e3c;
 }
-
 .disp-type.syncbase {
-  background-color: purple;
+  background-color: #d32f2f;
 }
diff --git a/start_syncbased.sh b/start_syncbased.sh
index a446dfc..5b16f65 100755
--- a/start_syncbased.sh
+++ b/start_syncbased.sh
@@ -3,8 +3,16 @@
 # 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:
+# Expects credentials in $TMPDIR/creds (where $TMPDIR defaults to /tmp),
+# 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 --v23.permissions.literal='{"Admin":{"In":["..."]},"Write":{"In":["..."]},"Read":{"In":["..."]},"Resolve":{"In":["..."]},"Debug":{"In":["..."]}}'
+set -euo pipefail
+
+TMPDIR=${TMPDIR-/tmp}
+
+mkdir -p $TMPDIR
+
+./bin/syncbased --root-dir=${TMPDIR}/syncbase --v23.tcp.address=localhost:8200 --v23.credentials=${TMPDIR}/creds --v23.permissions.literal='{"Admin":{"In":["..."]},"Write":{"In":["..."]},"Read":{"In":["..."]},"Resolve":{"In":["..."]},"Debug":{"In":["..."]}}'