TBR: todosapp: sync works!!!!1
except for one bug - see demo.md
Change-Id: I9de074adeb755fef64f33107053358d6660ab36e
diff --git a/Makefile b/Makefile
index 5cf4f9f..810b922 100644
--- a/Makefile
+++ b/Makefile
@@ -40,9 +40,12 @@
.DELETE_ON_ERROR:
+# Builds mounttabled, principal, and syncbased.
bin: $(shell $(FIND) $(V23_ROOT) -name "*.go")
+ v23 go build -a -o $@/mounttabled v.io/x/ref/services/mounttable/mounttabled
v23 go build -a -o $@/principal v.io/x/ref/cmd/principal
v23 go build -a -o $@/syncbased v.io/syncbase/x/ref/services/syncbase/syncbased
+ touch $@
node_modules: package.json $(shell $(FIND) $(V23_ROOT)/roadmap/javascript/syncbase/{package.json,src} $(V23_ROOT)/release/javascript/core/{package.json,src})
npm prune
diff --git a/README.md b/README.md
index dfe3066..e6fb7dd 100644
--- a/README.md
+++ b/README.md
@@ -1,37 +1,37 @@
# Todos app
-Todos is an example app that demonstrates use of [Syncbase][syncbase].
-
-Originally based on the [Meteor Todos demo app][meteor-todos].
+Todos is an example app that demonstrates use of [Syncbase][syncbase], and is
+originally based on the [Meteor Todos demo app][meteor-todos]. The high-level
+requirements for this app are [described here][requirements].
## Running the web application
The commands below assume that the current working directory is
-`$V23_ROOT/experimental/projects/todosapp`.
+`$V23_ROOT/experimental/projects/todosapp` and that you've installed the
+[Vanadium Chrome extension][crx].
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`.
+daemons (mounttabled and syncbased). Leave the blessing extension field empty.
./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`.
+Next, start local daemons (in another terminal). This script runs mounttabled
+and syncbased at ports `$PORT+1` and `$PORT+2` respectively, and configures
+Syncbase to persist its data under `tmp/syncbase_$PORT`. It expects to find
+credentials in `tmp/creds`.
- TMPDIR=tmp ./tools/start_syncbased.sh
+ PORT=4000 ./tools/start_services.sh
# Or, start from a clean slate.
- rm -rf tmp/syncbase* && TMPDIR=tmp ./tools/start_syncbased.sh
+ rm -rf tmp/syncbase* && PORT=4000 ./tools/start_services.sh
Finally, start the web app.
- DEBUG=1 make serve
+ DEBUG=1 PORT=4000 make serve
Visit `http://localhost:4000` in your browser to access the app.
@@ -42,30 +42,38 @@
Syncbase, add `d=syncbase` to the url query params, or simply click the storage
engine indicator in the top 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.
+When using Syncbase, by default the app attempts to contact the Vanadium object
+name `/localhost:($PORT+1)/syncbase`, where `/localhost:($PORT+1)` is the local
+mount table name and `syncbase` is the relative name of the Syncbase service. To
+specify a different mount table name, add `mt=<name>` to the url query params,
+e.g. `mt=/localhost:5001`. To specify a different Syncbase service name, add
+`sb=<name>`, e.g. `sb=/localhost:4002`.
-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.
+Beware that `start_services.sh` starts Syncbase with completely open ACLs. This
+is safe if Syncbase is only accessible on the local network (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 high-level requirements for this app are
-[described here][requirements].
+communicates with a local Syncbase daemon through the [Vanadium Chrome
+extension][crx]. The app UI is built with 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], and the v0 sync setup is
-[described here][demo-sync-setup]. 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.
+The data layout and conflict resolution policies for this app are [detailed
+here][design], and the v0 sync setup is [described here][demo-sync-setup]. The
+basic data layout is as follows, where `todos`, `db`, and `tb` are the Syncbase
+app, database, and table names respectively.
+
+ todos/db/tb/<listId> --> List
+ todos/db/tb/<listId>/todos/<todoId> --> Todo
+ todos/db/tb/<listId>/todos/<todoId>/tags/<tagName> --> nil
+
+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
@@ -96,21 +104,21 @@
Signature
- $V23_ROOT/release/go/bin/vrpc -v23.credentials=tmp/creds signature /localhost:8200
+ $V23_ROOT/release/go/bin/vrpc -v23.credentials=tmp/creds signature /localhost:4002
Method call
- $V23_ROOT/release/go/bin/vrpc -v23.credentials=tmp/creds call /localhost:8200 GetPermissions
- $V23_ROOT/release/go/bin/vrpc -v23.credentials=tmp/creds call /localhost:8200/todos/db/tb Scan '""' '""'
+ $V23_ROOT/release/go/bin/vrpc -v23.credentials=tmp/creds call /localhost:4002 GetPermissions
+ $V23_ROOT/release/go/bin/vrpc -v23.credentials=tmp/creds call /localhost:4002/todos/db/tb Scan '""' '""'
Glob
- $V23_ROOT/release/go/bin/namespace -v23.credentials=tmp/creds glob "/localhost:8200/..."
+ $V23_ROOT/release/go/bin/namespace -v23.credentials=tmp/creds glob "/localhost:4002/..."
Debug
- $V23_ROOT/release/go/bin/debug -v23.credentials=tmp/creds glob "/localhost:8200/__debug/stats/rpc/server/routing-id/..."
- $V23_ROOT/release/go/bin/debug -v23.credentials=tmp/creds stats read "/localhost:8200/__debug/stats/rpc/server/routing-id/c61964ab4c72ee522067eb6d5ddd22fc/methods/BeginBatch/latency-ms"
+ $V23_ROOT/release/go/bin/debug -v23.credentials=tmp/creds glob "/localhost:4002/__debug/stats/rpc/server/routing-id/..."
+ $V23_ROOT/release/go/bin/debug -v23.credentials=tmp/creds stats read "/localhost:4002/__debug/stats/rpc/server/routing-id/c61964ab4c72ee522067eb6d5ddd22fc/methods/BeginBatch/latency-ms"
### Integration test setup
@@ -130,25 +138,18 @@
$V23_ROOT/release/go/bin/namespace -v23.credentials=/usr/local/google/home/sadovsky/vanadium/roadmap/javascript/syncbase/tmp/test-credentials glob "/@5@ws@127.0.0.1:41249@7d24de5a57f6532b184562654ad2c554@m@test/child@@/test/syncbased/..."
-Visit `http://localhost:4000/?d=syncbase&n=test/syncbased` in the launched
+Visit `http://localhost:4000/?d=syncbase&sb=test/syncbased` in the launched
Chrome instance to talk to your test syncbase.
-To run a simple benchmark (100 puts, followed by a scan of those rows), specify
-query param `bm=1`.
+### Performance benchmarking
-### Benchmark performance
+To run a simple benchmark (parallel 100 puts, followed by a scan of those rows),
+specify query param `bm=1`.
All numbers assume dev console is closed, and assume non-test setup as described
above.
-Currently, parallel 100 puts takes 4s, and scanning 100 rows takes 0.6s.
-
-For the puts, avoiding unnecessarily cautious Signature RPC and avoiding 2x
-blowup from unnecessary Resolve calls will help. Parallelism doesn't help as
-much as one would hope, need to understand why.
-
-For the scan, 100ms comes from JS encode/decode, and probably much of the rest
-from WSPR. Needs further digging.
+Currently, parallel 100 puts takes 700ms, and scanning 100 rows takes 300ms.
[syncbase]: https://docs.google.com/document/d/12wS_IEPf8HTE7598fcmlN-Y692OWMSneoe2tvyBEpi0/edit#
[crx]: https://v.io/tools/vanadium-chrome-extension.html
diff --git a/browser/index.js b/browser/index.js
index 5b9b82c..c91704f 100644
--- a/browser/index.js
+++ b/browser/index.js
@@ -21,7 +21,6 @@
var DISP_TYPE_COLLECTION = 'mem-collection';
var DISP_TYPE_SYNCBASE = 'syncbase';
-var SYNCBASE_NAME = '/localhost:8200'; // default value
////////////////////////////////////////
// Global state
@@ -32,6 +31,16 @@
// Used for query params.
var u = url.parse(window.location.href, true);
+// Mount table name.
+var mt = u.query.mt || (function() {
+ var loc = window.location;
+ return '/' + loc.hostname + ':' + (Number(loc.port) + 1);
+}());
+
+// See TODO in SyncbaseDispatcher.listIdToSgName to understand why we use an
+// absolute name here.
+var syncbaseName = u.query.sb || (mt + '/syncbase');
+
////////////////////////////////////////
// Helpers
@@ -41,7 +50,7 @@
cb = util.logFn('initVanadium', cb);
var vanadiumConfig = {
logLevel: vanadium.vlog.levels.INFO,
- namespaceRoots: u.query.mounttable ? [u.query.mounttable] : undefined,
+ namespaceRoots: [mt],
proxy: u.query.proxy
};
vanadium.init(vanadiumConfig, cb);
@@ -381,8 +390,12 @@
},
render: function() {
var that = this, list = this.props.list, shared = Boolean(list.sg);
- var loc = window.location;
- var shareUrl = loc.origin + '/share/' + list._id + loc.search;
+ var shareUrl;
+ if (shared) {
+ var encodedSgName = util.strToHex(list.sg.name);
+ var loc = window.location;
+ shareUrl = loc.origin + '/share/' + encodedSgName + loc.search;
+ }
return h('div#status-pane', {
onClick: function(e) {
e.stopPropagation();
@@ -409,10 +422,10 @@
alert('Invalid email address.');
return;
}
- disp.createSyncGroup(list._id, [
+ disp.createSyncGroup(disp.listIdToSgName(list._id), [
emailToBlessing(userEmail),
emailToBlessing(value)
- ]);
+ ], mt);
},
cancel: function(e) {
e.target.value = '';
@@ -521,7 +534,10 @@
},
setListId_: function(listId) {
if (listId === this.state.listId) return;
- this.setState({listId: listId, tagFilter: null});
+ this.setState({
+ listId: listId,
+ tagFilter: null
+ });
},
updateURL: function() {
var listId = this.state.listId;
@@ -529,6 +545,45 @@
// Note, this doesn't trigger a re-render; it's purely visual.
window.history.replaceState({}, '', pathname + window.location.search);
},
+ // Updates state.lists. Calls cb once the setState call has completed.
+ // TODO(sadovsky): If possible, simplify how we deal with concurrent state
+ // updates, here and elsewhere. The current approach is fairly subtle and
+ // error-prone. Our goal is simple: never show stale data, even in the
+ // presence of sync.
+ updateLists_: function(cb) {
+ var that = this;
+ var listsSeq = this.state.lists.seq + 1;
+ this.getLists_(function(err, lists) {
+ if (err) return cb(err);
+ // Use setState(cb) form to ensure atomicity.
+ // References: https://goo.gl/CZ82Vp and https://goo.gl/vVCp8B
+ that.setState(function(state) {
+ if (listsSeq <= state.lists.seq) {
+ return {};
+ }
+ return {lists: {seq: listsSeq, items: lists}};
+ }, cb);
+ });
+ },
+ // Updates state.todos[listId]. Calls cb once the setState call has completed.
+ updateTodos_: function(listId, cb) {
+ var that = this;
+ var stateTodos = this.state.todos[listId];
+ var todosSeq = (stateTodos ? stateTodos.seq : 0) + 1;
+ this.getTodos_(listId, function(err, todos) {
+ if (err) return cb(err);
+ // Use setState(cb) form to ensure atomicity.
+ // References: https://goo.gl/CZ82Vp and https://goo.gl/vVCp8B
+ that.setState(function(state) {
+ var stateTodos = state.todos[listId];
+ if (stateTodos && todosSeq <= stateTodos.seq) {
+ return {};
+ }
+ state.todos[listId] = {seq: todosSeq, items: todos};
+ return {todos: state.todos};
+ }, cb);
+ });
+ },
componentWillMount: function() {
var that = this, props = this.props;
if (props.benchmark) {
@@ -540,22 +595,26 @@
});
return;
}
- function done() {
- that.setState({dispInitialized: true});
- }
initDispatcher(props.dispType, props.syncbaseName, function(err) {
if (err) throw err;
- if (props.joinListId) {
+ that.setState({dispInitialized: true}, function() {
+ if (!props.joinSgName) return;
+ // TODO(sadovsky): Show "please wait..." modal?
console.assert(props.dispType === DISP_TYPE_SYNCBASE);
- disp.joinSyncGroup(props.joinListId, function(err) {
+ disp.joinSyncGroup(props.joinSgName, 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();
+ var listId = disp.sgNameToListId(props.joinSgName);
+ // TODO(sadovsky): Wait for all items to get synced before attempting
+ // to read them?
+ that.updateTodos_(listId, function(err) {
+ if (err) throw err;
+ // Note, componentDidUpdate() will update the url.
+ that.setState({listId: listId});
+ });
});
- } else {
- done();
- }
+ });
});
},
componentDidMount: function() {
@@ -584,62 +643,22 @@
return listId;
}
- // Updates lists. Calls cb once the setState call has completed.
- // TODO(sadovsky): If possible, simplify how we deal with concurrent state
- // updates, here and elsewhere. The current approach is fairly subtle and
- // error-prone. Our goal is simple: never show stale data, even in the
- // presence of sync.
- function updateLists(cb) {
- var listsSeq = that.state.lists.seq + 1;
- that.getLists_(function(err, lists) {
- if (err) return cb(err);
- // Use setState(cb) form to ensure atomicity.
- // References: https://goo.gl/CZ82Vp and https://goo.gl/vVCp8B
- that.setState(function(state) {
- if (listsSeq <= state.lists.seq) {
- return {};
- }
- return {lists: {seq: listsSeq, items: lists}};
- }, cb);
- });
- }
-
- // Updates todos for the specified list. Calls cb once the setState call
- // has completed.
- function updateTodos(listId, cb) {
- var stateTodos = that.state.todos[listId];
- var todosSeq = (stateTodos ? stateTodos.seq : 0) + 1;
- that.getTodos_(listId, function(err, todos) {
- if (err) return cb(err);
- // Use setState(cb) form to ensure atomicity.
- // https://goo.gl/CZ82Vp
- that.setState(function(state) {
- var stateTodos = state.todos[listId];
- if (stateTodos && todosSeq <= stateTodos.seq) {
- return {};
- }
- state.todos[listId] = {seq: todosSeq, items: todos};
- return {todos: state.todos};
- }, cb);
- });
- }
-
// TODO(sadovsky): Only read (and only redraw) what's needed based on what
// changed.
disp.on('change', function() {
var doneCb = util.logFn('onChange', function(err) {
if (err) throw err;
});
- updateLists(function(err) {
+ that.updateLists_(function(err) {
if (err) throw err;
var listId = getListId();
- updateTodos(listId, doneCb);
+ that.updateTodos_(listId, doneCb);
});
});
// Load initial lists and todos. Note that changes can come in concurrently
// via sync.
- updateLists(function(err) {
+ this.updateLists_(function(err) {
if (err) throw err;
// Set initial listId if needed.
var listId = getListId();
@@ -648,7 +667,9 @@
}
// Get todos for all lists.
var listIds = _.pluck(that.state.lists.items, '_id');
- async.each(listIds, updateTodos, function(err) {
+ async.each(listIds, function(listId, cb) {
+ that.updateTodos_(listId, cb);
+ }, function(err) {
if (err) throw err;
});
});
@@ -671,7 +692,7 @@
if (that.props.dispType === DISP_TYPE_SYNCBASE) {
newDispType = DISP_TYPE_COLLECTION;
}
- window.location.href = '/?d=' + newDispType + '&n=' + SYNCBASE_NAME;
+ window.location.replace('/?d=' + newDispType);
}
}),
ListsPane({
@@ -688,9 +709,7 @@
// Note, setState just schedules render, so setListId's state update
// will be merged with ours.
that.setListId_(listId);
- that.setState({
- showStatusDialog: true
- });
+ that.setState({showStatusDialog: true});
}
}),
h('div#tags-and-todos-pane', {key: 'tags-and-todos-pane'}, [
@@ -727,32 +746,25 @@
// visibility of the log.
domLog.init();
-function render(listId, doJoin) {
- var dispType = u.query.d || DISP_TYPE_COLLECTION;
- var syncbaseName = u.query.n || SYNCBASE_NAME;
- var benchmark = Boolean(u.query.bm);
- var props = {
- initialListId: listId,
- dispType: dispType,
+function render(props) {
+ props = _.assign({
+ dispType: u.query.d || DISP_TYPE_COLLECTION,
syncbaseName: syncbaseName,
- benchmark: benchmark
- };
- if (doJoin) {
- props.joinListId = listId;
- }
+ benchmark: Boolean(u.query.bm)
+ }, props);
React.render(Page(props), document.querySelector('#page'));
}
// Configure Page.js routes. Note, ctx here is a Page.js context object, not a
// Vanadium context object.
page('/', function(ctx) {
- render(null, false);
+ render();
});
page('/lists/:listId', function(ctx) {
- render(ctx.params.listId, false);
+ render({initialListId: ctx.params.listId});
});
-page('/share/:listId', function(ctx) {
- render(ctx.params.listId, true);
+page('/share/:encodedSgName', function(ctx) {
+ render({joinSgName: util.hexToStr(ctx.params.encodedSgName)});
});
// Start Page.js router.
diff --git a/browser/syncbase_dispatcher.js b/browser/syncbase_dispatcher.js
index 8895f8e..2bea08c 100644
--- a/browser/syncbase_dispatcher.js
+++ b/browser/syncbase_dispatcher.js
@@ -146,7 +146,7 @@
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);
+ that.getSyncGroup_(ctx, sgName, cb);
}, cb);
});
});
@@ -282,20 +282,22 @@
// Currently, SG names must be of the form <syncbaseName>/$sync/<suffix>.
// We use <app>/<db>/<table>/<listId> for the suffix part.
-SyncbaseDispatcher.prototype.sgNameToListId_ = function(sgName) {
+SyncbaseDispatcher.prototype.sgNameToListId = function(sgName) {
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');
+SyncbaseDispatcher.prototype.listIdToSgName = function(listId) {
+ // TODO(sadovsky): fullName doesn't include the mount table name, i.e. the
+ // part corresponding to namespaceRoots. Our workaround is to specify a
+ // fully-qualified Syncbase name.
+ var prefix = this.tb_.fullName.replace('/todos/db/tb', '/$sync/todos/db/tb');
return prefix + '/' + 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);
+// Returns an object describing the SyncGroup with the given name.
+// Currently, this object will have two fields: 'name' and 'spec'.
+define('getSyncGroup_', function(ctx, sgName, cb) {
+ var sg = this.db_.syncGroup(sgName);
async.parallel([
function(cb) {
sg.getSpec(ctx, function(err, spec, version) {
@@ -318,7 +320,8 @@
], function(err, results) {
if (err) return cb(err);
cb(null, {
- spec: results[0],
+ name: sgName,
+ spec: results[0]
});
});
});
@@ -329,8 +332,8 @@
syncPriority: 8
});
-define('createSyncGroup', function(ctx, listId, blessings, cb) {
- var sgName = this.listIdToSgName_(listId), sg = this.db_.syncGroup(sgName);
+define('createSyncGroup', function(ctx, sgName, blessings, mt, cb) {
+ var sg = this.db_.syncGroup(sgName);
var spec = new nosql.SyncGroupSpec({
// TODO(sadovsky): Maybe make perms more restrictive.
perms: new Map([
@@ -341,13 +344,14 @@
['Debug', {'in': blessings}]
]),
// TODO(sadovsky): Update this once we switch to {table, prefix} tuples.
- prefixes: ['tb:' + listId]
+ prefixes: ['tb:' + this.sgNameToListId(sgName)],
+ mountTables: [vanadium.naming.join(mt, 'rendezvous')]
});
sg.create(ctx, spec, MEMBER_INFO, this.maybeEmit_(cb));
});
-define('joinSyncGroup', function(ctx, listId, cb) {
- var sgName = this.listIdToSgName_(listId), sg = this.db_.syncGroup(sgName);
+define('joinSyncGroup', function(ctx, sgName, cb) {
+ var sg = this.db_.syncGroup(sgName);
sg.join(ctx, MEMBER_INFO, this.maybeEmit_(cb));
});
diff --git a/browser/util.js b/browser/util.js
index 1234937..76f8346 100644
--- a/browser/util.js
+++ b/browser/util.js
@@ -36,6 +36,14 @@
return randomBytes(Math.ceil(len / 2)).toString('hex').substr(0, len);
};
+// Converts from binary to hex-encoded string and vice versa.
+exports.strToHex = function(s) {
+ return new Buffer(s, 'binary').toString('hex');
+}
+exports.hexToStr = function(s) {
+ return new Buffer(s, 'hex').toString('binary');
+}
+
function logStart(name) {
console.log(name + ' start');
return Date.now();
diff --git a/demo.md b/demo.md
new file mode 100644
index 0000000..61ce4be
--- /dev/null
+++ b/demo.md
@@ -0,0 +1,68 @@
+# Demo setup
+
+This page describes how to set things up for a demo.
+For detailed explanations of the setup steps, see [README.md](README.md).
+
+FIXME: Currently, once anything is deleted, outgoing sync permanently stops
+working.
+
+## Single-machine setup
+
+Run these commands once:
+
+ DEBUG=1 make build
+ ./bin/principal seekblessings --v23.credentials tmp/creds
+
+Run these commands (each from its own terminal) on each reset:
+
+ rm -rf tmp/syncbase* && PORT=5000 ./tools/start_services.sh
+ PORT=5100 ./tools/start_services.sh
+
+ DEBUG=1 PORT=5000 make serve
+ DEBUG=1 PORT=5100 make serve
+
+Open these urls:
+
+ http://localhost:5000/?d=syncbase // Alice
+ http://localhost:5100/?d=syncbase // Bob
+
+### Syncing a list
+
+1. In Alice's window, create list "Groceries".
+2. Add todo items and tags (as desired).
+3. Click the status button, then type in Bob's email address.
+4. Copy the `/share/...` part of the url to the clipboard.
+5. Switch to Bob's window.
+6. Replace everything after `localhost:5100` with the copied path, hit enter.
+7. After a second, Bob should see the synced "Groceries" list.
+8. Add, edit, and remove todos and tags to your heart's content and watch sync
+ do its magic.
+
+## Two-machine setup
+
+Have Alice and Bob do the following on their respective machines.
+
+Run these commands once:
+
+ DEBUG=1 make build
+ ./bin/principal seekblessings --v23.credentials tmp/creds
+
+Run these commands (each from its own terminal) on each reset:
+
+ rm -rf tmp/syncbase* && PORT=5000 ./tools/start_services.sh
+
+ DEBUG=1 PORT=5000 make serve
+
+Open this url:
+
+ http://localhost:5000/?d=syncbase
+
+### Syncing a list
+
+1. In Alice's window, create list "Groceries".
+2. Add todo items and tags (as desired).
+3. Click the status button, then type in Bob's email address.
+4. Send Bob the entire url, and have him open that url.
+5. After a second, Bob should see the synced "Groceries" list.
+6. Add, edit, and remove todos and tags to your heart's content and watch sync
+ do its magic.
diff --git a/server.js b/server.js
index 1117234..237e3e5 100644
--- a/server.js
+++ b/server.js
@@ -16,6 +16,6 @@
res.sendFile(pathTo('public/index.html'));
});
-var server = app.listen(4000, function() {
+var server = app.listen(process.env.PORT || 4000, function() {
console.log('Serving http://localhost:%d', server.address().port);
});
diff --git a/stylesheets/index.less b/stylesheets/index.less
index 468e796..7bf81b5 100644
--- a/stylesheets/index.less
+++ b/stylesheets/index.less
@@ -205,6 +205,7 @@
.shared-with, .url {
margin-top: 24px;
+ overflow-wrap: break-word;
}
.subtitle {
diff --git a/tools/start_services.sh b/tools/start_services.sh
new file mode 100755
index 0000000..80c38fc
--- /dev/null
+++ b/tools/start_services.sh
@@ -0,0 +1,55 @@
+#!/bin/bash
+
+# Expects credentials in $TMPDIR/creds (where $TMPDIR defaults to /tmp),
+# generated as follows:
+#
+# make build
+# ./bin/principal seekblessings --v23.credentials tmp/creds
+
+set -euo pipefail
+
+trap kill_child_processes INT TERM EXIT
+
+silence() {
+ "$@" &> /dev/null || true
+}
+
+# Copied from chat example app.
+kill_child_processes() {
+ # Attempt to stop child processes using the TERM signal.
+ if [[ -n "$(jobs -p -r)" ]]; then
+ silence pkill -P $$
+ sleep 1
+ # Kill any remaining child processes using the KILL signal.
+ if [[ -n "$(jobs -p -r)" ]]; then
+ silence sudo -u "${SUDO_USER}" pkill -9 -P $$
+ fi
+ fi
+}
+
+main() {
+ local -r TMPDIR=tmp
+ local -r PORT=${PORT-4000}
+ local -r MOUNTTABLED_ADDR="localhost:$((PORT+1))"
+ local -r SYNCBASED_ADDR="localhost:$((PORT+2))"
+
+ mkdir -p $TMPDIR
+
+ # TODO(sadovsky): Run mounttabled and syncbased each with its own blessing
+ # extension.
+ ./bin/mounttabled \
+ --v23.tcp.address=${MOUNTTABLED_ADDR} \
+ --v23.credentials=${TMPDIR}/creds &
+
+ ./bin/syncbased \
+ --root-dir=${TMPDIR}/syncbase_${PORT} \
+ --name=syncbase \
+ --v23.namespace.root=/${MOUNTTABLED_ADDR} \
+ --v23.tcp.address=${SYNCBASED_ADDR} \
+ --v23.credentials=${TMPDIR}/creds \
+ --v23.permissions.literal='{"Admin":{"In":["..."]},"Write":{"In":["..."]},"Read":{"In":["..."]},"Resolve":{"In":["..."]},"Debug":{"In":["..."]}}'
+
+ tail -f /dev/null # wait forever
+}
+
+main "$@"
diff --git a/tools/start_syncbased.sh b/tools/start_syncbased.sh
deleted file mode 100755
index 2e45010..0000000
--- a/tools/start_syncbased.sh
+++ /dev/null
@@ -1,20 +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.
-
-# Expects credentials in $TMPDIR/creds (where $TMPDIR defaults to /tmp),
-# generated as follows:
-#
-# make build
-# ./bin/principal seekblessings --v23.credentials tmp/creds
-
-set -euo pipefail
-
-TMPDIR=${TMPDIR-/tmp}
-
-mkdir -p $TMPDIR
-
-set -x
-
-./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":["..."]}}' # --enable-sync=0