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