Starting Travelsync integration
Change-Id: I07b0ddf1060c4a5f4b9167597e7ddb29f384530e
diff --git a/.gitignore b/.gitignore
index 21261d6..36dd53c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,3 +2,5 @@
/ifc
/node_modules
/server-root
+/bin
+/tmp
\ No newline at end of file
diff --git a/Makefile b/Makefile
index 90a9d76..7da5f6a 100644
--- a/Makefile
+++ b/Makefile
@@ -1,5 +1,5 @@
PATH := node_modules/.bin:$(PATH)
-PATH := $(PATH):$(V23_ROOT)/third_party/cout/node/bin
+PATH := $(PATH):$(V23_ROOT)/third_party/cout/node/bin:$(V23_ROOT)/release/go/bin
.DEFAULT_GOAL := all
@@ -9,12 +9,12 @@
server_static := $(patsubst src/static/%,server-root/%,$(wildcard src/static/*))
tests := $(patsubst %.js,%,$(shell find test -name "*.js"))
-out_dirs := ifc server-root node_modules
+out_dirs := ifc server-root node_modules bin
.DELETE_ON_ERROR:
.PHONY: all
-all: static js
+all: static js bin
@true
.PHONY: static
@@ -23,14 +23,20 @@
.PHONY: js
js: server-root/bundle.js
+bin:
+ @v23 go build -a -o $@/syncbased v.io/syncbase/x/ref/services/syncbase/syncbased
+ @touch $@
+
ifc: src/ifc/*
@VDLPATH=src vdl generate -lang=javascript -js-out-dir=. ifc
node_modules: package.json
@npm prune
@npm install
- @npm install $(V23_ROOT)/release/javascript/core/ #TODO: remove
- @touch node_modules # if npm does nothing, we don't want to keep trying
+ @ # TODO(rosswang): remove these two
+ @npm install $(V23_ROOT)/release/javascript/core/
+ @npm install $(V23_ROOT)/roadmap/javascript/syncbase/
+ @touch $@ # if npm does nothing, we don't want to keep trying
server-root:
@mkdir server-root
@@ -57,6 +63,24 @@
start: all
@static server-root -p $(port)
+.PHONY: bootstrap
+bootstrap: creds syncbase
+
+.PHONY: creds
+creds:
+ @principal seekblessings --v23.credentials tmp/creds
+
+.PHONY: syncbase
+syncbase: bin
+ @bash ./tools/start_services.sh
+
+.PHONY: clean-all
+clean-all: clean clean-tmp
+
.PHONY: clean
clean:
rm -rf $(out_dirs)
+
+.PHONY: clean-tmp
+clean-tmp:
+ rm -rf tmp
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..ab650b9
--- /dev/null
+++ b/README.md
@@ -0,0 +1,46 @@
+# Travel Planner
+
+An example travel planner using Vanadium.
+
+## Dependencies
+
+If you have a `$V23_ROOT` setup you can install Node.js from
+`$V23_ROOT/third_party` by running:
+
+ v23 profile install nodejs
+
+Optionally, it is possible to use your own install of Node.js if you would like
+to use a more recent version.
+
+In order to run the local syncbase instance via `make bootstrap` or related
+targets, you will need to ensure that the standard Vanadium binaries have been
+built by running:
+
+ v23 go install v.io/...
+
+## Building
+
+The default make task will install any modules listed in the `package.json` and
+build a browser bundle from `src/index.js` via browserify.
+
+ make
+
+It is possible to have the build happen automatically anytime a JavaScript file
+changes using the watch tool:
+
+ watch make
+
+## Running locally
+
+Local instances require a blessed syncbase instance. To attain blessings and
+start syncbase, use:
+
+ make bootstrap
+
+To run a local dev server use:
+
+ make start
+
+If you would like to change the host and or port that is used:
+
+ make start port=<port>
diff --git a/mocks/vanadium.js b/mocks/vanadium.js
index f1354a2..7b4b725 100644
--- a/mocks/vanadium.js
+++ b/mocks/vanadium.js
@@ -3,6 +3,7 @@
// license that can be found in the LICENSE file.
var defineClass = require('../src/util/define-class');
+var Deferred = require('vanadium/src/lib/deferred');
var MockRuntime = defineClass({
publics: {
@@ -29,11 +30,16 @@
publics: {
init: function(config, callback) {
this.t.ok(config, 'has config');
- this.callback = callback;
+ this.deferred = new Deferred(callback);
+ return this.deferred.promise;
},
finishInit: function(err, runtime) {
- this.callback(err, runtime);
+ if (err) {
+ this.deferred.reject(err);
+ } else {
+ this.deferred.resolve(runtime);
+ }
}
},
diff --git a/package.json b/package.json
index 1c4c638..9a0ee71 100644
--- a/package.json
+++ b/package.json
@@ -15,6 +15,7 @@
"dependencies": {
"global": "^4.3.0",
"jquery": "^2.1.4",
+ "es6-promisify": "^2.0.0",
"uuid": "^2.0.1"
}
}
diff --git a/src/components/message.js b/src/components/message.js
index 49c08a9..e51c489 100644
--- a/src/components/message.js
+++ b/src/components/message.js
@@ -8,23 +8,25 @@
var INFO = 'INFO';
var ERROR = 'ERROR';
+function info(text) {
+ return {
+ type: INFO,
+ text: text
+ };
+}
+
+function error(text) {
+ return {
+ type: ERROR,
+ text: text
+ };
+}
+
module.exports = {
INFO: INFO,
ERROR: ERROR,
-
- info: function(text) {
- return {
- type: INFO,
- text: text
- };
- },
-
- error: function(text) {
- return {
- type: ERROR,
- text: text
- };
- },
+ info: info,
+ error: error,
Message: defineClass({
publics: {
@@ -46,12 +48,39 @@
},
set: function(message) {
+ if (!message) {
+ this.onLowerPriority();
+ return;
+ }
+
+ if (typeof message === 'string') {
+ message = info(message);
+ }
+
+ var self = this;
+
this.setType(message.type);
this.setText(message.text);
+
+ if (message.promise) {
+ message.promise.then(function(message) {
+ self.set(message);
+ }, function(err) {
+ self.set(error(err));
+ });
+ } else {
+ this.onLowerPriority();
+ }
}
},
- constants: ['$'],
+ constants: [ '$' ],
+ events: {
+ /**
+ * Event raised when the message is no longer pending user action.
+ */
+ onLowerPriority: 'memory once'
+ },
init: function(initial) {
this.$ = $('<li>');
diff --git a/src/components/messages.js b/src/components/messages.js
index 0b39920..5c5c956 100644
--- a/src/components/messages.js
+++ b/src/components/messages.js
@@ -13,30 +13,31 @@
TTL: 9000,
FADE: 1000,
SLIDE_UP: 300,
- OPEN_CLOSE: 500
+ OPEN_CLOSE: 400
},
publics: {
close: function() {
var self = this;
+ var $messages = this.$messages;
if (this.isOpen()) {
- if (this.$messages.children().length) {
+ if ($messages.children().length) {
var scrollOffset =
- this.$messages.scrollTop() + self.$messages.height();
+ $messages.scrollTop() + $messages.height();
this.$messages
.addClass('animating')
.animate({ height: 0 }, {
duration: this.OPEN_CLOSE,
progress: function() {
- self.$messages.scrollTop(
- scrollOffset - self.$messages.height());
+ $messages.scrollTop(
+ scrollOffset - $messages.height());
},
complete: function() {
- self.$messages.removeClass('animating');
+ $messages.removeClass('animating');
self.$.addClass('headlines');
- self.$messages.attr('style', null);
+ $messages.attr('style', null);
}
});
} else {
@@ -46,8 +47,7 @@
},
isClosed: function() {
- return this.$.hasClass('headlines') &&
- !this.$messages.hasClass('animating');
+ return this.$.hasClass('headlines');
},
isOpen: function() {
@@ -56,28 +56,28 @@
},
open: function() {
- var self = this;
+ var $messages = this.$messages;
if (!this.isOpen()) {
- var $animating = this.$.find('.animating');
- $animating.stop(true);
- $animating.removeClass('animating');
- $animating.attr('style', null);
+ this.$.find('.animating')
+ .stop(true)
+ .removeClass('animating')
+ .attr('style', null);
this.$.removeClass('headlines');
- if (this.$messages.children().length) {
- var goalHeight = this.$messages.height();
- this.$messages
+ if ($messages.children().length) {
+ var goalHeight = $messages.height();
+ $messages
.addClass('animating')
.height(0)
.animate({ height: goalHeight }, {
duration: this.OPEN_CLOSE,
progress: function() {
- self.$messages.scrollTop(self.$messages.prop('scrollHeight'));
+ $messages.scrollTop($messages.prop('scrollHeight'));
},
complete: function() {
- self.$messages.removeClass('animating');
- self.$messages.attr('style', null);
+ $messages.removeClass('animating');
+ $messages.attr('style', null);
}
});
}
@@ -85,6 +85,8 @@
},
push: function(messageData) {
+ var self = this;
+
var messageObject = new message.Message(messageData);
this.$messages.append(messageObject.$);
@@ -107,17 +109,28 @@
* It would be best to use CSS animations, but at this time that would
* mean sacrificing either auto-height or flow-affecting sliding.
*/
- messageObject.$.addClass('animating');
messageObject.$
- .slideDown(this.SLIDE_DOWN)
- .delay(this.TTL)
- .animate({ opacity: 0 }, this.FADE)
- .slideUp(this.SLIDE_UP, function() {
- messageObject.$
- .removeClass('animating')
- .attr('style', null);
- });
+ .addClass('animating')
+ .hide()
+ .slideDown(this.SLIDE_DOWN);
}
+
+ messageObject.onLowerPriority.add(function() {
+ messageObject.$.addClass('history');
+
+ if (self.isClosed()) {
+ messageObject.$
+ .addClass('animating')
+ .show()
+ .delay(Messages.TTL)
+ .animate({ opacity: 0 }, Messages.FADE)
+ .slideUp(Messages.SLIDE_UP, function() {
+ messageObject.$
+ .removeClass('animating')
+ .attr('style', null);
+ });
+ }
+ });
},
toggle: function() {
diff --git a/src/static/index.css b/src/static/index.css
index f62f541..f7dad01 100644
--- a/src/static/index.css
+++ b/src/static/index.css
@@ -75,11 +75,12 @@
.messages .handle {
background-color: rgba(192, 192, 192, .95);
- border: 1px solid #aaa;
+ border-bottom: 1px solid #aaa;
border-radius: 2px;
color: #444;
cursor: pointer;
text-align: center;
+ transition: background-color .2s, border-bottom .2s;
-webkit-touch-callout: none;
-webkit-user-select: none;
@@ -91,7 +92,8 @@
.messages.headlines .handle {
background-color: rgba(255, 255, 255, .5);
- border: initial;
+ border-bottom: initial;
+ transition: background-color .2s, border-bottom .2s;
}
.handle:before {
@@ -130,10 +132,13 @@
background-color: rgba(0, 0, 0, .6);
border-radius: 4px;
color: white;
- display: none;
margin-top: 3px;
}
+.messages.headlines li.history {
+ display: none;
+}
+
.messages li:before {
font-weight: bold;
}
diff --git a/src/strings.js b/src/strings.js
index a6a76d6..f73188a 100644
--- a/src/strings.js
+++ b/src/strings.js
@@ -4,6 +4,8 @@
function getStrings(locale) {
return {
+ 'Connected to all services.': 'Connected to all services.',
+ 'Connecting...': 'Connecting...',
'Destination': 'Destination',
destination: function(n) {
return 'Destination ' + n;
diff --git a/src/travel.js b/src/travel.js
index f511382..939bca6 100644
--- a/src/travel.js
+++ b/src/travel.js
@@ -29,8 +29,10 @@
this.map.message(message.error(err.toString()));
},
- info: function (info) {
- this.map.message(message.info(info));
+ info: function (info, promise) {
+ var messageData = message.info(info);
+ messageData.promise = promise;
+ this.map.message(messageData);
}
},
@@ -39,20 +41,24 @@
var vanadiumWrapper = opts.vanadiumWrapper || vanadiumWrapperDefault;
var travel = this;
+ this.map = new Map(opts);
this.sync = new TravelSync();
var reportError = $.proxy(this, 'error');
- vanadiumWrapper.init(opts.vanadium).then(
- function(wrapper) {
+ this.info(strings['Connecting...'], vanadiumWrapper.init(opts.vanadium)
+ .then(function(wrapper) {
wrapper.onCrash.add(reportError);
var identity = new Identity(wrapper.getAccountName());
identity.mountName = makeMountName(identity);
- travel.sync.start(identity.mountName, wrapper).catch(reportError);
- }, reportError);
-
- this.map = new Map(opts);
+ return travel.sync.start(identity.mountName, wrapper);
+ }).then(function() {
+ return strings['Connected to all services.'];
+ }, function(err) {
+ console.error(err);
+ throw err;
+ }));
var directionsServiceStatusStrings = buildStatusErrorStringMap(
this.map.maps.DirectionsStatus, strings.DirectionsStatus);
diff --git a/src/travelsync.js b/src/travelsync.js
index 5caf3e5..1717000 100644
--- a/src/travelsync.js
+++ b/src/travelsync.js
@@ -2,17 +2,90 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
+//TODO(rosswang): either expect ES6, use our own polyfill, or export this in V23
+var Promise = require('vanadium/src/lib/promise');
+
+var $ = require('./util/jquery');
var defineClass = require('./util/define-class');
var vdlTravel = require('../ifc');
var TravelSync = defineClass({
- events: ['onMessage', 'onPlanUpdate', 'onStatusUpdate'],
+ publics: {
+ start: function(mountName, v) {
+ var self = this;
+ var startSyncbase = v.syncbase('/localhost:4001/syncbase').then(
+ function(syncbase) {
+ self.syncbase = syncbase;
+ syncbase.onError.add(self.onError);
+ syncbase.onUpdate.add(self.processUpdates.bind(self));
+ });
+
+ return Promise.all([
+ v.server(mountName, this.server),
+ startSyncbase
+ ]);
+ },
+
+ message: function(messageContent) {
+
+ },
+
+ pushTrip: function() {
+ },
+
+ pushStatus: function() {
+ }
+ },
+
+ privates: {
+ marshal: function(x) {
+ return JSON.stringify(x);
+ },
+
+ unmarshal: function(x) {
+ return JSON.parse(x);
+ },
+
+ processUpdates: function(data) {
+ var self = this;
+ if (data.messages) {
+ /* Dispatch new messages in time order, though don't put them before
+ * local messages. */
+ var newMessages = [];
+ $.each(data.messages, function(id, serializedMessage) {
+ if (!self.messages[id]) {
+ var message = self.unmarshal(serializedMessage);
+ newMessages.push(message);
+ self.messages[id] = message;
+ }
+ });
+ newMessages.sort(function(a, b) {
+ return a.timestamp < b.timestamp? -1 :
+ a.timestamp > b.timestamp? 1 :
+ 0;
+ });
+
+ self.onMessages(newMessages);
+ }
+ }
+ },
+
+ events: {
+ onError: 'memory',
+ /**
+ * @param messages array of {content, timestamp} pair objects.
+ */
+ onMessages: '',
+ onPlanUpdate: '',
+ onStatusUpdate: ''
+ },
+
init: function() {
this.tripPlan = [];
this.tripStatus = {};
+ this.messages = {};
- // TODO: sync initial state
this.server = new vdlTravel.TravelSync();
var travelSync = this;
@@ -33,15 +106,6 @@
travelSync.tripStatus = status;
travelSync.onStatusUpdate(status);
};
- },
- publics: {
- start: function(mountName, v) {
- return v.server(mountName, this.server);
- },
- pushTrip: function() {
- },
- pushStatus: function() {
- }
}
});
diff --git a/src/vanadium-wrapper.js b/src/vanadium-wrapper/index.js
similarity index 78%
rename from src/vanadium-wrapper.js
rename to src/vanadium-wrapper/index.js
index 0662bad..d6f52c4 100644
--- a/src/vanadium-wrapper.js
+++ b/src/vanadium-wrapper/index.js
@@ -2,10 +2,10 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
-var $ = require('./util/jquery');
-
var vanadiumDefault = require('vanadium');
-var defineClass = require('./util/define-class');
+var defineClass = require('../util/define-class');
+
+var SyncbaseWrapper = require('./syncbase-wrapper');
var VanadiumWrapper = defineClass({
init: function(runtime) {
@@ -34,6 +34,13 @@
*/
server: function(endpoint, server) {
return this.runtime.newServer().serve(endpoint, server);
+ },
+
+ /**
+ * @param endpoint Vanadium name
+ */
+ syncbase: function(endpoint) {
+ return SyncbaseWrapper.start(this.runtime.getContext(), endpoint);
}
},
@@ -56,16 +63,8 @@
appName: 'Travel Planner'
};
- var async = $.Deferred();
-
- vanadium.init(config, function(err, runtime) {
- if (err) {
- async.reject(err);
- } else {
- async.resolve(new VanadiumWrapper(runtime));
- }
+ return vanadium.init(config).then(function(runtime) {
+ return new VanadiumWrapper(runtime);
});
-
- return async.promise();
}
};
diff --git a/src/vanadium-wrapper/syncbase-wrapper.js b/src/vanadium-wrapper/syncbase-wrapper.js
new file mode 100644
index 0000000..a7f6cba
--- /dev/null
+++ b/src/vanadium-wrapper/syncbase-wrapper.js
@@ -0,0 +1,117 @@
+// 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.
+
+var promisify = require('es6-promisify');
+var syncbase = require('syncbase');
+
+var defineClass = require('../util/define-class');
+
+/**
+ * Create app, db, and table structure in Syncbase.
+ */
+function setUp(context, app, db) {
+ function nonfatals(err) {
+ switch (err.id) {
+ case 'v.io/v23/verror.Exist':
+ console.info(err.msg);
+ return;
+ default:
+ throw err;
+ }
+ }
+
+ //TODO(rosswang) If {} will remain empty, can it be omitted?
+ return promisify(app.create.bind(app))(context, {})
+ .catch(nonfatals)
+ .then(function() {
+ return promisify(db.create.bind(db))(context, {});
+ })
+ .catch(nonfatals)
+ .then(function() {
+ return promisify(db.createTable.bind(db))(context, 't', {});
+ })
+ .catch(nonfatals);
+}
+
+/**
+ * Translate Syncbase hierarchical keys to object structure for easier
+ * processing. '.' is chosen as the separator; '/' is reserved in Syncbase.
+ */
+function recursiveSet(root, key, value) {
+ var matches = /\.?([^\.]*)(.*)/.exec(key);
+ var member = matches[1];
+ var remaining = matches[2];
+
+ if (remaining) {
+ var child = root[member];
+ if (!child) {
+ child = root[member] = {};
+ }
+ recursiveSet(child, remaining, value);
+ } else {
+ root[member] = value;
+ }
+}
+
+var SyncbaseWrapper = defineClass({
+ statics: {
+ start: function(context, mountName) {
+ var service = syncbase.newService(mountName);
+ var app = service.app('travel');
+ var db = app.noSqlDatabase('db');
+
+ return setUp(context, app, db).then(function() {
+ return new SyncbaseWrapper(context, db);
+ });
+ }
+ },
+
+ publics: {
+ refresh: function() {
+ var self = this;
+ var isHeader = true;
+
+ var query = 'select k, v from t';
+ var newData = {};
+ this.db.exec(this.context, query, function(err) {
+ if (err) {
+ self.onError(err);
+ } else {
+ self.data = newData;
+ self.onUpdate(newData);
+ }
+ }).on('data', function(row) {
+ if (isHeader) {
+ isHeader = false;
+ } else {
+ recursiveSet(newData, row[0], row[1]);
+ }
+ }).on('error', function(err) {
+ self.onError(err);
+ });
+ }
+ },
+
+ events: {
+ onError: 'memory',
+ onUpdate: 'memory'
+ },
+
+ init: function(context, db) {
+ var self = this;
+ this.context = context;
+ this.db = db;
+ this.data = {};
+
+ // Start the watch loop to periodically poll for changes from sync.
+ // TODO(rosswang): Remove this once we have client watch.
+ this.watchLoop = function() {
+ self.refresh();
+ setTimeout(self.watchLoop, 500);
+ };
+ process.nextTick(self.watchLoop);
+ }
+});
+
+module.exports = SyncbaseWrapper;
diff --git a/test/travel.js b/test/travel.js
index 2aca67c..b4d3ded 100644
--- a/test/travel.js
+++ b/test/travel.js
@@ -24,27 +24,6 @@
cleanDom();
});
-test('message display', function(t) {
- var travel = new Travel({
- vanadiumWrapper: mockVanadiumWrapper,
- maps: mockMaps
- });
-
- var $messages = $('.messages ul');
- t.ok($messages.length, 'message display exists');
- t.equals($messages.children().length, 0, 'message display is empty');
-
- travel.info('Test message.');
-
- var $messageItem = $messages.children();
- t.equals($messageItem.length, 1, 'message display shows 1 message');
- t.equals($messageItem.text(), 'Test message.',
- 'message displays message text');
-
- t.end();
- cleanDom();
-});
-
test('domRoot', function(t) {
var $root = $('<div>');
var root = $root[0];
diff --git a/test/vanadium-wrapper.js b/test/vanadium-wrapper.js
index 19fbf4f..6ff997d 100644
--- a/test/vanadium-wrapper.js
+++ b/test/vanadium-wrapper.js
@@ -26,9 +26,10 @@
}
};
- vanadiumWrapper.init(mockVanadium).then(
+ var promise = vanadiumWrapper.init(mockVanadium).then(
function(v) {
context.vanadiumWrapper = v;
+ return context;
},
function(err) {
t.fail('init error');
@@ -36,26 +37,27 @@
mockVanadium.finishInit(null, mockRuntime);
- return context;
+ return promise;
}
test('crashBefore', function(t) {
- var crashTest = setUpCrashTest(t);
+ setUpCrashTest(t).then(function(crashTest) {
+ crashTest.crash('I lost the game.');
+ crashTest.bindCrashHandler();
+ t.equal(crashTest.crashErr, 'I lost the game.');
- crashTest.crash('I lost the game.');
- crashTest.bindCrashHandler();
- t.equal(crashTest.crashErr, 'I lost the game.');
-
- t.end();
+ t.end();
+ });
});
test('crashAfter', function(t) {
- var crashTest = setUpCrashTest(t);
- crashTest.bindCrashHandler();
- t.notOk(crashTest.crashErr, 'no crash yet');
+ setUpCrashTest(t).then(function(crashTest) {
+ crashTest.bindCrashHandler();
+ t.notOk(crashTest.crashErr, 'no crash yet');
- crashTest.crash('I lost the game.');
- t.equal(crashTest.crashErr, 'I lost the game.');
+ crashTest.crash('I lost the game.');
+ t.equal(crashTest.crashErr, 'I lost the game.');
- t.end();
+ t.end();
+ });
});
\ No newline at end of file
diff --git a/tools/start_services.sh b/tools/start_services.sh
new file mode 100644
index 0000000..6432e60
--- /dev/null
+++ b/tools/start_services.sh
@@ -0,0 +1,49 @@
+#!/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 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 TMP=tmp
+ local -r PORT=${PORT-4000}
+ local -r MOUNTTABLED_ADDR=":$((PORT+1))"
+ local -r SYNCBASED_ADDR=":$((PORT+2))"
+ mkdir -p $TMP
+ # TODO(rosswang): Run mounttabled and syncbased each with its own blessing
+ # extension.
+ ${V23_ROOT}/release/go/bin/mounttabled \
+ --v23.tcp.address=${MOUNTTABLED_ADDR} \
+ --v23.credentials=${TMP}/creds &
+ ./bin/syncbased \
+ --v=5 \
+ --alsologtostderr=false \
+ --root-dir=${TMP}/syncbase_${PORT} \
+ --name=syncbase \
+ --v23.namespace.root=/${MOUNTTABLED_ADDR} \
+ --v23.tcp.address=${SYNCBASED_ADDR} \
+ --v23.credentials=${TMP}/creds \
+ --v23.permissions.literal='{"Admin":{"In":["..."]},"Write":{"In":["..."]},"Read":{"In":["..."]},"Resolve":{"In":["..."]},"Debug":{"In":["..."]}}'
+ tail -f /dev/null # wait forever
+}
+main "$@"