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":["..."]}}'