Merge branch 'master' of ../../../experimental
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..8165887
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,11 @@
+# Build artifacts
+/bin
+/creds
+/gen
+/dart/lib/gen
+
+# Dart artifacts
+.packages
+.pub
+pubspec.lock
+packages
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..f51a72d
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,234 @@
+SHELL := /bin/bash -euo pipefail
+PWD := $(shell pwd)
+V23_GOPATH := $(shell echo `v23 run env | grep GOPATH | cut -d\= -f2`)
+DART_FILES := $(shell find dart/bin dart/lib dart/test sky_demo/lib -name "*.dart" -not -path "dart/lib/gen/*")
+GO_FILES := $(shell find go/src -name "*.go")
+V23_GO_FILES := $(shell find $(V23_ROOT) -name "*.go")
+
+# Flags for Syncbase service running as Mojo service.
+# See v.io/x/ref/runtime/internal/mojo_util.go for the wonderful magic that
+# makes this work.
+# If you change these flags, be sure to do a "make clean", since the flag values
+# get bound at compile time.
+SYNCBASED_ADDR := 127.0.0.1:4002
+V23_MOJO_FLAGS := --v=0 --v23.tcp.address=$(SYNCBASED_ADDR) --v23.permissions.literal={\"Admin\":{\"In\":[\"...\"]},\"Write\":{\"In\":[\"...\"]},\"Read\":{\"In\":[\"...\"]},\"Resolve\":{\"In\":[\"...\"]},\"Debug\":{\"In\":[\"...\"]}}
+
+# MOUNTTABLE_ADDR := 127.0.0.1:4001
+ifdef MOUNTTABLE_ADDR
+ V23_MOJO_FLAGS += --name=syncbase_mojo --v23.namespace.root=/$(MOUNTTABLE_ADDR)
+endif
+
+ifdef ANDROID
+ # Configure compiler and linker for Android.
+ export GOROOT := $(MOJO_DIR)/src/third_party/go/tool/android_arm
+ export CGO_ENABLED := 1
+ export GOOS := android
+ export GOARCH := arm
+ export GOARM := 7
+
+ ANDROID_NDK := $(V23_ROOT)/third_party/android/ndk-toolchain
+
+ export CC := $(ANDROID_NDK)/bin/arm-linux-androideabi-gcc
+ export CXX := $(ANDROID_NDK)/bin/arm-linux-androideabi-g++
+
+ MOJO_ANDROID_FLAGS := --android
+ MOJO_BUILD_DIR := $(MOJO_DIR)/src/out/android_Debug
+ MOJO_SHARED_LIB := $(PWD)/gen/lib/android/libsystem_thunk.a
+ MOJO_SHELL_PATH := $(MOJO_BUILD_DIR)/apks/MojoShell.apk
+ SKY_BUILD_DIR := $(SKY_DIR)/src/out/android_Debug
+ ETHER_BUILD_DIR := $(PWD)/gen/mojo/android
+
+ THIRD_PARTY_LIBS := $(V23_ROOT)/third_party/cout/android_arm
+
+ # NOTE(nlacasse): Trying to write to a directory that the app does not have
+ # permission to causes a crash with no stack trace. Because of this, we
+ # set logtostderr=true to prevent vlog from writing logs to directories we
+ # don't have permissions on. (Alternatively, we could set --log_dir to a
+ # directory inside APP_HOME_DIR.) We set syncbase root-dir inside
+ # APP_HOME_DIR for the same reason.
+ APP_HOME_DIR = /data/data/org.chromium.mojo.shell/app_home
+ V23_MOJO_FLAGS += --logtostderr=true --root-dir=$(APP_HOME_DIR)/syncbase_data
+else
+ # Configure compiler and linker for Linux.
+ export GOROOT := $(MOJO_DIR)/src/third_party/go/tool/linux_amd64
+
+ MOJO_BUILD_DIR := $(MOJO_DIR)/src/out/Debug
+ MOJO_SHARED_LIB := $(PWD)/gen/lib/linux_amd64/libsystem_thunk.a
+ MOJO_SHELL_PATH := $(MOJO_BUILD_DIR)/mojo_shell
+ SKY_BUILD_DIR := $(SKY_DIR)/src/out/Debug
+ ETHER_BUILD_DIR := $(PWD)/gen/mojo/linux_amd64
+
+ THIRD_PARTY_LIBS := $(V23_ROOT)/third_party/cout/linux_amd64
+
+ V23_MOJO_FLAGS += --root-dir=/tmp/syncbase_data
+endif
+
+GOPATH := $(V23_GOPATH):$(MOJO_DIR):$(MOJO_DIR)/third_party/go:$(MOJO_BUILD_DIR)/gen/go:$(PWD)/go:$(PWD)/gen/go
+
+# NOTE(nlacasse): Running Go Mojo services requires passing the
+# --enable-multiprocess flag to mojo_shell. This is because the Go runtime is
+# very large, and can interfere with C++ memory if they are in the same
+# process.
+MOJO_SHELL_FLAGS := -v --enable-multiprocess \
+ --config-alias MOJO_BUILD_DIR=$(MOJO_BUILD_DIR) \
+ --config-alias SKY_DIR=$(SKY_DIR) \
+ --config-alias SKY_BUILD_DIR=$(SKY_BUILD_DIR) \
+ --config-alias ETHER_DIR=$(PWD) \
+ --config-alias ETHER_BUILD_DIR=$(ETHER_BUILD_DIR)
+
+LDFLAGS := -shared
+
+# Compiles a Go program and links against the Mojo C library.
+# $1 is input filename.
+# $2 is output filename.
+define MOGO_BUILD
+ mkdir -p $(dir $2)
+ GOPATH="$(GOPATH)" \
+ CGO_CFLAGS="-I$(MOJO_DIR)/src $(CGO_CFLAGS)" \
+ CGO_CXXFLAGS="-I$(MOJO_DIR)/src $(CGO_CXXFLAGS)" \
+ CGO_LDFLAGS="-L$(dir $(MOJO_SHARED_LIB)) -lsystem_thunk $(CGO_LDFLAGS)" \
+ $(GOROOT)/bin/go build -o $2 -tags=mojo -ldflags="$(LDFLAGS)" -buildmode=c-shared $1
+ rm -f $(basename $2).h
+endef
+
+# Generates go bindings from .mojom file.
+# $1 is input filename.
+# $2 is output directory.
+# $3 is language (go, dart, ...).
+define MOJOM_GEN
+ mkdir -p $2
+ $(MOJO_DIR)/src/mojo/public/tools/bindings/mojom_bindings_generator.py $1 -d . -o $2 -g $3
+endef
+
+.DELETE_ON_ERROR:
+
+all: test
+
+.PHONY: build
+build: $(ETHER_BUILD_DIR)/echo_server.mojo $(ETHER_BUILD_DIR)/syncbase_server.mojo
+
+# Builds mounttabled, principal, and syncbased.
+bin: $(V23_GO_FILES) | env-check
+ 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 $@
+
+# Mints credentials.
+creds: bin
+ ./bin/principal seekblessings --v23.credentials creds
+ touch $@
+
+# Builds the library that Mojo services must be linked with.
+$(MOJO_SHARED_LIB): | env-check
+ mkdir -p $(dir $@)
+ ar rcs $@ $(MOJO_BUILD_DIR)/obj/mojo/public/platform/native/system.system_thunks.o
+
+.PHONY: gen-mojom
+gen-mojom: dart/lib/gen/dart-pkg/mojom/lib/mojo/echo.mojom.dart gen/go/src/mojom/echo/echo.mojom.go
+gen-mojom: dart/lib/gen/dart-pkg/mojom/lib/mojo/syncbase.mojom.dart gen/go/src/mojom/syncbase/syncbase.mojom.go
+
+dart/lib/gen/dart-pkg/mojom/lib/mojo/echo.mojom.dart: mojom/echo.mojom
+dart/lib/gen/dart-pkg/mojom/lib/mojo/syncbase.mojom.dart: mojom/syncbase.mojom
+dart/lib/gen/dart-pkg/mojom/lib/mojo/echo.mojom.dart dart/lib/gen/dart-pkg/mojom/lib/mojo/syncbase.mojom.dart: | env-check
+ $(call MOJOM_GEN,$<,dart/lib/gen,dart)
+ # TODO(nlacasse): mojom_bindings_generator creates bad symlinks on dart
+ # files, so we delete them. Stop doing this once the generator is fixed.
+ # See https://github.com/domokit/mojo/issues/386
+ rm -f dart/lib/gen/mojom/$(notdir $@)
+
+gen/go/src/mojom/echo/echo.mojom.go: mojom/echo.mojom
+gen/go/src/mojom/syncbase/syncbase.mojom.go: mojom/syncbase.mojom
+gen/go/src/mojom/echo/echo.mojom.go gen/go/src/mojom/syncbase/syncbase.mojom.go: | env-check
+ $(call MOJOM_GEN,$<,gen,go)
+ gofmt -w $@
+
+$(ETHER_BUILD_DIR)/echo_server.mojo: $(GO_FILES) $(MOJO_SHARED_LIB) gen/go/src/mojom/echo/echo.mojom.go | env-check
+ $(call MOGO_BUILD,$(PWD)/go/src/echo_server.go,$@)
+
+# TODO(nlacasse): These target-specific variables will affect this task and all
+# pre-requisite tasks. Luckily none of the prerequisites require that these
+# variables have their original value, so everything works. Once we have a
+# prereq that requires the original value, we will need to re-work these
+# variables.
+$(ETHER_BUILD_DIR)/syncbase_server.mojo: CGO_CFLAGS := -I$(THIRD_PARTY_LIBS)/leveldb/include
+$(ETHER_BUILD_DIR)/syncbase_server.mojo: CGO_CXXFLAGS := -I$(THIRD_PARTY_LIBS)/leveldb/include
+$(ETHER_BUILD_DIR)/syncbase_server.mojo: CGO_LDFLAGS := -L$(THIRD_PARTY_LIBS)/leveldb/lib -lleveldb -L$(THIRD_PARTY_LIBS)/snappy/lib -lsnappy
+$(ETHER_BUILD_DIR)/syncbase_server.mojo: LDFLAGS := -X v.io/x/ref/runtime/internal.commandLineFlags '$(V23_MOJO_FLAGS)'
+$(ETHER_BUILD_DIR)/syncbase_server.mojo: $(GO_FILES) $(V23_GO_FILES) $(MOJO_SHARED_LIB) gen/go/src/mojom/syncbase/syncbase.mojom.go | env-check
+ $(call MOGO_BUILD,v.io/syncbase/x/ref/services/syncbase/syncbased,$@)
+
+# Formats dart files to follow dart style conventions.
+.PHONY: dartfmt
+dartfmt:
+ dartfmt --overwrite $(DART_FILES)
+
+# Lints src and test files with dartanalyzer. This takes a few seconds.
+.PHONY: dartanalyzer
+dartanalyzer: dart/packages sky_demo/packages gen-mojom
+ # TODO(nlacasse): Fix dart mojom binding generator so it does not produce
+ # files that violate dartanalyzer. For now, we use "grep -v" to hide all
+ # hints and warnings from *.mojom.dart files.
+ cd dart && dartanalyzer bin/*.dart lib/*.dart test/*.dart | grep -v "\.mojom\.dart, line"
+ cd sky_demo && dartanalyzer lib/*.dart | grep -v "\.mojom\.dart, line"
+
+# Installs dart dependencies.
+dart/packages: dart/pubspec.yaml
+ cd dart && pub get
+
+# Installs dart dependencies.
+sky_demo/packages: sky_demo/pubspec.yaml
+ cd sky_demo && pub get
+
+.PHONY: run-syncbase-example
+run-syncbase-example: $(ETHER_BUILD_DIR)/syncbase_server.mojo dart/packages dart/lib/gen/dart-pkg/mojom/lib/mojo/syncbase.mojom.dart | env-check
+ $(MOJO_DIR)/src/mojo/devtools/common/mojo_run --config-file $(PWD)/mojoconfig $(MOJO_SHELL_FLAGS) $(MOJO_ANDROID_FLAGS) https://mojo.v.io/syncbase_example.dart
+
+.PHONY: run-echo-example
+run-echo-example: $(ETHER_BUILD_DIR)/echo_server.mojo dart/packages dart/lib/gen/dart-pkg/mojom/lib/mojo/echo.mojom.dart | env-check
+ $(MOJO_DIR)/src/mojo/devtools/common/mojo_run --config-file $(PWD)/mojoconfig $(MOJO_SHELL_FLAGS) $(MOJO_ANDROID_FLAGS) https://mojo.v.io/echo_example.dart
+
+.PHONY: run-sky-demo
+run-sky-demo: $(ETHER_BUILD_DIR)/echo_server.mojo sky_demo/packages $(ETHER_BUILD_DIR)/syncbase_server.mojo dart/lib/gen/dart-pkg/mojom/lib/mojo/echo.mojom.dart dart/lib/gen/dart-pkg/mojom/lib/mojo/syncbase.mojom.dart | env-check
+ $(MOJO_DIR)/src/mojo/devtools/common/mojo_run --config-file $(PWD)/sky_demo/mojoconfig $(MOJO_SHELL_FLAGS) $(MOJO_ANDROID_FLAGS) 'mojo:window_manager https://mojo.v.io/sky_demo/lib/main.dart'
+
+.PHONY: test
+test: dart/packages $(ETHER_BUILD_DIR)/echo_server.mojo $(ETHER_BUILD_DIR)/syncbase_server.mojo gen-mojom | env-check
+ $(MOJO_DIR)/src/mojo/devtools/common/mojo_test --config-file $(PWD)/mojoconfig $(MOJO_SHELL_FLAGS) $(MOJO_ANDROID_FLAGS) --shell-path $(MOJO_SHELL_PATH) tests
+
+.PHONY: env-check
+env-check:
+ifndef MOJO_DIR
+ $(error MOJO_DIR is not set)
+endif
+ifndef SKY_DIR
+ $(error SKY_DIR is not set)
+endif
+ifndef V23_ROOT
+ $(error V23_ROOT is not set)
+endif
+ifeq ($(wildcard $(MOJO_BUILD_DIR)),)
+ $(error ERROR: $(MOJO_BUILD_DIR) does not exist. Please see README.md for instructions on compiling Mojo resources.)
+endif
+ifeq ($(wildcard $(THIRD_PARTY_LIBS)/*),)
+ ifdef ANDROID
+ $(error ERROR: $(THIRD_PARTY_LIBS) does not exist or is empty. Please run "GOOS=android GOARCH=arm v23 profile install syncbase")
+ else
+ $(error ERROR: $(THIRD_PARTY_LIBS) does not exist or is empty. Please run "v23 profile install syncbase")
+ endif
+endif
+ifdef ANDROID
+ ifeq ($(wildcard $(ANDROID_NDK)),)
+ $(error ERROR: $(ANDROID_NDK) does not exist. Please install android profile with "v23 profile install android")
+ endif
+endif
+
+.PHONY: clean
+clean:
+ rm -rf gen/mojo gen/go
+ rm -rf dart/lib/gen
+
+.PHONY: veryclean
+veryclean: clean
+ rm -rf gen
+ rm -rf {dart,sky_demo}/{.packages,pubspec.lock,packages}
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..e21f655
--- /dev/null
+++ b/README.md
@@ -0,0 +1,100 @@
+# Ether
+
+This project exposes Syncbase as a Mojo service.
+
+Read the [architecture proposal].
+
+## Initial Mojo setup
+
+You must have the Mojo repo in `$MOJO_DIR`.
+
+This section only needs to be run once.
+
+See the [Mojo readme] for more comprehensive instructions.
+
+### Install Mojo prereqs
+
+1. Install [depot tools].
+2. Install [Goma][goma].
+3. Put the following in your `.bashrc`:
+
+ # NOTE: Actual locations depend on where you installed depot_tools and
+ # goma.
+ export PATH=${PATH}:${HOME}/dev/depot_tools
+ export GOMA_DIR=${HOME}/goma
+ export MOJO_DIR=${HOME}/mojo
+
+### Download Mojo repo
+
+ $ mkdir $MOJO_DIR && cd $MOJO_DIR
+
+ # NOTE: This step takes about 10 min.
+ $ fetch mojo --target_os=android,linux
+
+ # NOTE: This step also takes about 10 min. Furthermore, the script uses
+ # 'sudo', so you will need to enter your password.
+ $ cd src && ./build/install-build-deps.sh
+
+ # Or, to include Android deps as well:
+ $ cd src && ./build/install-build-deps-android.sh
+
+## Update Mojo and compile resources
+
+This updates the Mojo repo to HEAD, and builds the Mojo resources needed to
+compile Ether.
+
+Run this while you grab your morning coffee.
+
+1. Start by updating the repo.
+
+ $ cd $MOJO_DIR/src
+ $ git checkout master
+ $ git pull
+ $ gclient sync
+
+2. Compile for Linux. Built resources will be in `$MOJO_DIR/src/out/Debug`
+
+ $ ./mojo/tools/mojob.py gn
+ $ ./mojo/tools/mojob.py build # NOTE: This can take up to 10 minutes.
+
+3. Compile for Android. Built resources will be in
+ `$MOJO_DIR/src/out/android_Debug`
+
+ $ ./mojo/tools/mojob.py gn --android
+ $ ./mojo/tools/mojob.py build --android # NOTE: This can take up to 10 minutes.
+
+## Sky setup
+
+You must have the Sky code in `$SKY_DIR`. Follow the instructions in the Sky
+[CONTRIBUTING.md][sky contrib] file for "Getting the code" and "Building the
+code".
+
+Also, you must patch your Mojo code (and rebuild Mojo):
+https://github.com/domokit/mojo/issues/370
+
+## Install Dart SDK
+
+To run Dart apps, you must install the Dart SDK.
+
+Googlers: http://go/install-dart
+External: https://www.dartlang.org/downloads/
+
+## Testing
+
+Run the tests:
+
+ make test
+
+This will run all tests listed in the `tests` file in the root directory of
+this repo.
+
+The following command will run a single test file. This is useful when the
+full test suite hangs with no output.
+
+ $(MOJO_DIR)/src/mojo/devtools/common/mojo_run -v --enable-multiprocess --shell-path $(MOJO_DIR)/src/out/Debug/mojo_shell dart/test/<filename>
+
+[architecture proposal]: https://docs.google.com/document/d/1TyxPYIhj9VBCtY7eAXu_MEV9y0dtRx7n7UY4jm76Qq4/edit
+[depot tools]: http://www.chromium.org/developers/how-tos/install-depot-tools
+[goma]: https://sites.google.com/a/google.com/goma/how-to-use-goma/how-to-use-goma-for-chrome-team
+[mojo readme]: https://github.com/domokit/mojo/blob/master/README.md
+[sky contrib]: https://github.com/domokit/sky_engine/blob/master/CONTRIBUTING.md
diff --git a/dart/bin/echo_example.dart b/dart/bin/echo_example.dart
new file mode 100755
index 0000000..226f1fb
--- /dev/null
+++ b/dart/bin/echo_example.dart
@@ -0,0 +1,18 @@
+#!mojo mojo:dart_content_handler
+import 'package:ether/initialized_application.dart' show InitializedApplication;
+import 'package:ether/echo_client.dart' show EchoClient;
+
+main(List args) async {
+ InitializedApplication app = new InitializedApplication.fromHandle(args[0]);
+ await app.initialized;
+
+ EchoClient c = new EchoClient(
+ app.connectToService, 'https://mojo.v.io/echo_server.mojo');
+
+ String input = 'foobar';
+ String output = await c.echo(input);
+ print('in=$input out=$output match=${input == output}');
+
+ await c.close();
+ await app.close();
+}
diff --git a/dart/bin/syncbase_example.dart b/dart/bin/syncbase_example.dart
new file mode 100644
index 0000000..21a6627
--- /dev/null
+++ b/dart/bin/syncbase_example.dart
@@ -0,0 +1,17 @@
+#!mojo mojo:dart_content_handler
+import 'package:ether/initialized_application.dart' show InitializedApplication;
+import 'package:ether/syncbase_client.dart' show SyncbaseClient;
+
+main(List args) async {
+ InitializedApplication app = new InitializedApplication.fromHandle(args[0]);
+ await app.initialized;
+
+ SyncbaseClient c = new SyncbaseClient(
+ app.connectToService, 'https://mojo.v.io/syncbase_server.mojo');
+
+ bool exists = await c.app('foo').exists();
+ print('app(foo).exists(): $exists');
+
+ await c.close();
+ await app.close();
+}
diff --git a/dart/lib/echo_client.dart b/dart/lib/echo_client.dart
new file mode 100644
index 0000000..64869be
--- /dev/null
+++ b/dart/lib/echo_client.dart
@@ -0,0 +1,34 @@
+library echo_client;
+
+import 'dart:async';
+
+import 'package:mojo/bindings.dart' as bindings;
+
+import 'gen/dart-gen/mojom/lib/mojo/echo.mojom.dart' as mojom;
+
+typedef void ConnectToServiceFn(String url, bindings.ProxyBase proxy);
+
+class EchoClient {
+ final mojom.EchoProxy _proxy;
+
+ EchoClient(ConnectToServiceFn cts, String url)
+ : _proxy = new mojom.EchoProxy.unbound() {
+ print('connecting to $url');
+ cts(url, _proxy);
+ print('connected');
+ }
+
+ // TODO(nlacasse): Is this necessary?
+ Future close({bool immediate: false}) {
+ return _proxy.close(immediate: immediate);
+ }
+
+ Future<String> echo(String s) async {
+ print('calling echoString($s)');
+ mojom.EchoEchoStringResponseParams v = await _proxy.ptr.echoString(s);
+
+ String output = v.value;
+ print('got echo result: $output');
+ return output;
+ }
+}
diff --git a/dart/lib/initialized_application.dart b/dart/lib/initialized_application.dart
new file mode 100644
index 0000000..4447e6f
--- /dev/null
+++ b/dart/lib/initialized_application.dart
@@ -0,0 +1,20 @@
+library initialized_application;
+
+import 'dart:async';
+
+import 'package:mojo/application.dart' show Application;
+import 'package:mojo/core.dart' show MojoHandle;
+
+// InitializedApplication is an Application with a future 'initialized' that is
+// resolved after the 'initialize' method finishes.
+class InitializedApplication extends Application {
+ final _initializeCompleter = new Completer();
+ Future get initialized => _initializeCompleter.future;
+
+ InitializedApplication.fromHandle(int handle)
+ : super.fromHandle(new MojoHandle(handle));
+
+ void initialize(List<String> args, String url) {
+ _initializeCompleter.complete();
+ }
+}
diff --git a/dart/lib/src/app.dart b/dart/lib/src/app.dart
new file mode 100644
index 0000000..6eb61ab
--- /dev/null
+++ b/dart/lib/src/app.dart
@@ -0,0 +1,41 @@
+part of syncbase_client;
+
+class SyncbaseApp extends NamedResource {
+ SyncbaseApp._internal(_proxy, fullName)
+ : super._internal(_proxy, null, fullName);
+
+ // noSqlDatabase returns the noSqlDatabase with the given relativeName.
+ // relativeName must not contain slashes.
+ SyncbaseNoSqlDatabase noSqlDatabase(String relativeName) {
+ return new SyncbaseNoSqlDatabase._internal(_proxy, fullName, relativeName);
+ }
+
+ Future create(mojom.Perms perms) async {
+ var v = await _proxy.ptr.appCreate(fullName, perms);
+ if (isError(v.err)) throw v.err;
+ }
+
+ Future delete() async {
+ var v = await _proxy.ptr.appDelete(fullName);
+ if (isError(v.err)) throw v.err;
+ }
+
+ Future<bool> exists() async {
+ var v = await _proxy.ptr.appExists(fullName);
+ if (isError(v.err)) throw v.err;
+ return v.exists;
+ }
+
+ Future<mojom.Perms> getPermissions() async {
+ var v = await _proxy.ptr.appGetPermissions(fullName);
+ if (isError(v.err)) throw v.err;
+ // TODO(nlacasse): We need to return the version too. Create a struct type
+ // that combines perms and version?
+ return v.perms;
+ }
+
+ Future setPermissions(mojom.Perms perms, String version) async {
+ var v = await _proxy.ptr.appSetPermissions(fullName, perms, version);
+ if (isError(v.err)) throw v.err;
+ }
+}
diff --git a/dart/lib/src/named_resource.dart b/dart/lib/src/named_resource.dart
new file mode 100644
index 0000000..9b51356
--- /dev/null
+++ b/dart/lib/src/named_resource.dart
@@ -0,0 +1,19 @@
+part of syncbase_client;
+
+// NamedResource is the superclass of resources with names.
+class NamedResource {
+ final String fullName;
+ final String relativeName;
+ final mojom.SyncbaseProxy _proxy;
+
+ NamedResource._internal(
+ mojom.SyncbaseProxy _proxy, String _parentFullName, String relativeName)
+ : this._proxy = _proxy,
+ this.relativeName = relativeName,
+ this.fullName = (_parentFullName == null ? '' : _parentFullName + '/') +
+ relativeName {
+ if (relativeName.contains('/')) {
+ throw 'relativeName cannot contain "/": $relativeName';
+ }
+ }
+}
diff --git a/dart/lib/src/nosql/database.dart b/dart/lib/src/nosql/database.dart
new file mode 100644
index 0000000..afb76ea
--- /dev/null
+++ b/dart/lib/src/nosql/database.dart
@@ -0,0 +1,104 @@
+part of syncbase_client;
+
+class SyncbaseNoSqlDatabase extends NamedResource {
+ SyncbaseNoSqlDatabase._internal(_proxy, _parentFullName, relativeName)
+ : super._internal(_proxy, _parentFullName, relativeName);
+
+ // table returns a table with the given relativeName. relativeName must not
+ // contain slashes.
+ SyncbaseTable table(String relativeName) {
+ return new SyncbaseTable._internal(_proxy, fullName, relativeName);
+ }
+
+ // syncGroup returns a syncGroup with the given name.
+ SyncbaseSyncGroup syncGroup(String name) {
+ return new SyncbaseSyncGroup._internal(_proxy, this.fullName, name);
+ }
+
+ Future<List<String>> getSyncGroupNames() async {
+ var v = await _proxy.ptr.dbGetSyncGroupNames(fullName);
+ if (isError(v.err)) throw v.err;
+ return v.names;
+ }
+
+ Future create(mojom.Perms perms) async {
+ var v = await _proxy.ptr.dbCreate(fullName, perms);
+ if (isError(v.err)) throw v.err;
+ }
+
+ Future delete() async {
+ var v = await _proxy.ptr.dbDelete(fullName);
+ if (isError(v.err)) throw v.err;
+ }
+
+ Future<bool> exists() async {
+ var v = await _proxy.ptr.dbExists(fullName);
+ if (isError(v.err)) throw v.err;
+ return v.exists;
+ }
+
+ Stream<mojom.Result> exec(String query) {
+ StreamController<mojom.Result> sc = new StreamController();
+ mojom.ExecStream execStream = new ExecStreamImpl._fromStreamController(sc);
+
+ // Call dbExec asynchronously.
+ _proxy.ptr.dbExec(fullName, query, execStream).then((v) {
+ // TODO(nlacasse): Is throwing the correct behavior here? Consider
+ // returning a tuple (Stream<mojom.Result>, Future) and resolve the
+ // Future at the end of the RPC (with an error if applicable). Then
+ // errors will be handled the same in this method as in all the other
+ // methods that return Futures. (Even though the other methods seem to
+ // "throw", they are actually resolving a Future since the function is
+ // declared with "async".)
+ if (isError(v.err)) throw v.err;
+ });
+
+ return sc.stream;
+ }
+
+ // TODO(nlacasse): Make a BatchDatabase class similar to what we did in JS.
+ Future<String> beginBatch(mojom.BatchOptions opts) async {
+ var v = await _proxy.ptr.dbBeginBatch(fullName, opts);
+ if (isError(v.err)) throw v.err;
+ return v.batchDn;
+ }
+
+ Future commit() async {
+ var v = await _proxy.ptr.dbCommit(fullName);
+ if (isError(v.err)) throw v.err;
+ }
+
+ Future abort() async {
+ var v = await _proxy.ptr.dbAbort(fullName);
+ if (isError(v.err)) throw v.err;
+ }
+
+ Future<mojom.Perms> getPermissions() async {
+ var v = await _proxy.ptr.dbGetPermissions(fullName);
+ if (isError(v.err)) throw v.err;
+ // TODO(nlacasse): We need to return the version too. Create a struct type
+ // that combines perms and version?
+ return v.perms;
+ }
+
+ Future setPermissions(mojom.Perms perms, String version) async {
+ var v = await _proxy.ptr.dbSetPermissions(fullName, perms, version);
+ if (isError(v.err)) throw v.err;
+ }
+}
+
+class ExecStreamImpl implements mojom.ExecStream {
+ final StreamController<mojom.Result> sc;
+ ExecStreamImpl._fromStreamController(this.sc);
+
+ onResult(mojom.Result result) {
+ sc.add(result);
+ }
+
+ onDone(mojom.Error err) {
+ if (isError(err)) {
+ sc.addError(err);
+ }
+ sc.close();
+ }
+}
diff --git a/dart/lib/src/nosql/row.dart b/dart/lib/src/nosql/row.dart
new file mode 100644
index 0000000..882f0b5
--- /dev/null
+++ b/dart/lib/src/nosql/row.dart
@@ -0,0 +1,28 @@
+part of syncbase_client;
+
+class SyncbaseRow extends NamedResource {
+ SyncbaseRow._internal(_proxy, _parentFullName, relativeName)
+ : super._internal(_proxy, _parentFullName, relativeName);
+
+ Future<bool> exists() async {
+ var v = await _proxy.ptr.rowExists(fullName);
+ if (isError(v.err)) throw v.err;
+ return v.exists;
+ }
+
+ Future<List<int>> get() async {
+ var v = await _proxy.ptr.rowGet(fullName);
+ if (isError(v.err)) throw v.err;
+ return v.value;
+ }
+
+ Future put(List<int> value) async {
+ var v = await _proxy.ptr.rowPut(fullName, value);
+ if (isError(v.err)) throw v.err;
+ }
+
+ Future delete() async {
+ var v = await _proxy.ptr.rowDelete(fullName);
+ if (isError(v.err)) throw v.err;
+ }
+}
diff --git a/dart/lib/src/nosql/syncgroup.dart b/dart/lib/src/nosql/syncgroup.dart
new file mode 100644
index 0000000..65ecc68
--- /dev/null
+++ b/dart/lib/src/nosql/syncgroup.dart
@@ -0,0 +1,54 @@
+part of syncbase_client;
+
+class SyncbaseSyncGroup {
+ final String name;
+ final String _dbName;
+ final mojom.SyncbaseProxy _proxy;
+
+ SyncbaseSyncGroup._internal(this._proxy, this._dbName, this.name);
+
+ Future create(
+ mojom.SyncGroupSpec spec, mojom.SyncGroupMemberInfo myInfo) async {
+ var v = await _proxy.ptr.dbCreateSyncGroup(_dbName, name, spec, myInfo);
+ if (isError(v.err)) throw v.err;
+ }
+
+ Future join(mojom.SyncGroupMemberInfo myInfo) async {
+ var v = await _proxy.ptr.dbJoinSyncGroup(_dbName, name, myInfo);
+ if (isError(v.err)) throw v.err;
+ }
+
+ Future leave() async {
+ var v = await _proxy.ptr.dbLeaveSyncGroup(_dbName, name);
+ if (isError(v.err)) throw v.err;
+ }
+
+ Future destroy() async {
+ var v = await _proxy.ptr.dbDestroySyncGroup(_dbName, name);
+ if (isError(v.err)) throw v.err;
+ }
+
+ Future eject(String memberName) async {
+ var v = await _proxy.ptr.dbEjectFromSyncGroup(_dbName, name, memberName);
+ if (isError(v.err)) throw v.err;
+ }
+
+ Future<mojom.SyncGroupSpec> getSpec() async {
+ var v = await _proxy.ptr.dbGetSyncGroupSpec(_dbName, name);
+ if (isError(v.err)) throw v.err;
+ // TODO(nlacasse): We need to return the version too. Create a struct type
+ // that combines spec and version?
+ return v.spec;
+ }
+
+ Future setSpec(mojom.SyncGroupSpec spec, String version) async {
+ var v = await _proxy.ptr.dbSetSyncGroupSpec(_dbName, name, spec, version);
+ if (isError(v.err)) throw v.err;
+ }
+
+ Future<Map<String, mojom.SyncGroupMemberInfo>> getMembers() async {
+ var v = await _proxy.ptr.dbGetSyncGroupMembers(_dbName, name);
+ if (isError(v.err)) throw v.err;
+ return v.infos;
+ }
+}
diff --git a/dart/lib/src/nosql/table.dart b/dart/lib/src/nosql/table.dart
new file mode 100644
index 0000000..d3409ec
--- /dev/null
+++ b/dart/lib/src/nosql/table.dart
@@ -0,0 +1,84 @@
+part of syncbase_client;
+
+class SyncbaseTable extends NamedResource {
+ SyncbaseTable._internal(_proxy, _parentFullName, relativeName)
+ : super._internal(_proxy, _parentFullName, relativeName);
+
+ SyncbaseRow row(String key) {
+ return new SyncbaseRow._internal(_proxy, fullName, key);
+ }
+
+ Future create(mojom.Perms perms) async {
+ var v = await _proxy.ptr.tableCreate(fullName, perms);
+ if (isError(v.err)) throw v.err;
+ }
+
+ Future delete() async {
+ var v = await _proxy.ptr.tableDelete(fullName);
+ if (isError(v.err)) throw v.err;
+ }
+
+ Future<bool> exists() async {
+ var v = await _proxy.ptr.tableExists(fullName);
+ if (isError(v.err)) throw v.err;
+ return v.exists;
+ }
+
+ Future deleteRowRange(List<int> start, List<int> limit) async {
+ var v = await _proxy.ptr.tableDeleteRowRange(fullName, start, limit);
+ if (isError(v.err)) throw v.err;
+ }
+
+ Stream<mojom.KeyValue> scan(List<int> start, List<int> limit) {
+ StreamController<mojom.KeyValue> sc = new StreamController();
+
+ mojom.ScanStreamStub stub = new mojom.ScanStreamStub.unbound();
+ stub.impl = new ScanStreamImpl._fromStreamController(sc);
+
+ // Call tableScan asynchronously.
+ _proxy.ptr.tableScan(fullName, start, limit, stub).then((v) {
+ // TODO(nlacasse): Is throwing the correct behavior here? Consider
+ // returning a tuple (Stream<mojom.KeyValue>, Future) and resolve the
+ // Future at the end of the RPC (with an error if applicable). Then
+ // errors will be handled the same in this method as in all the other
+ // methods that return Futures. (Even though the other methods seem to
+ // "throw", they are actually resolving a Future since the function is
+ // declared with "async".)
+ if (isError(v.err)) throw v.err;
+ });
+
+ return sc.stream;
+ }
+
+ Future<List<mojom.PrefixPerms>> getPermissions(String key) async {
+ var v = await _proxy.ptr.tableGetPermissions(fullName, key);
+ if (isError(v.err)) throw v.err;
+ return v.permsArr;
+ }
+
+ Future setPermissions(String prefix, mojom.Perms perms) async {
+ var v = await _proxy.ptr.tableSetPermissions(fullName, prefix, perms);
+ if (isError(v.err)) throw v.err;
+ }
+
+ Future deletePermissions(String prefix) async {
+ var v = await _proxy.ptr.tableDeletePermissions(fullName, prefix);
+ if (isError(v.err)) throw v.err;
+ }
+}
+
+class ScanStreamImpl implements mojom.ScanStream {
+ final StreamController<mojom.KeyValue> sc;
+ ScanStreamImpl._fromStreamController(this.sc);
+
+ onKeyValue(mojom.KeyValue keyValue) {
+ sc.add(keyValue);
+ }
+
+ onDone(mojom.Error err) {
+ if (isError(err)) {
+ sc.addError(err);
+ }
+ sc.close();
+ }
+}
diff --git a/dart/lib/syncbase_client.dart b/dart/lib/syncbase_client.dart
new file mode 100644
index 0000000..a61f687
--- /dev/null
+++ b/dart/lib/syncbase_client.dart
@@ -0,0 +1,60 @@
+library syncbase_client;
+
+import 'dart:async';
+
+import 'package:mojo/bindings.dart' as bindings;
+
+import 'gen/dart-gen/mojom/lib/mojo/syncbase.mojom.dart' as mojom;
+
+// Export struct types from syncbase.mojom.
+// TODO(nlacasse): Create wrapper around Perms, and possibly other struct
+// constructors, since the default constructors are not user-friendly. They
+// take zero arguments, so all fields must be populated with assignments.
+export 'gen/dart-gen/mojom/lib/mojo/syncbase.mojom.dart'
+ show BatchOptions, Perms, SyncGroupMemberInfo, SyncGroupSpec;
+
+part 'src/app.dart';
+part 'src/named_resource.dart';
+part 'src/nosql/database.dart';
+part 'src/nosql/row.dart';
+part 'src/nosql/syncgroup.dart';
+part 'src/nosql/table.dart';
+
+typedef void ConnectToServiceFn(String url, bindings.ProxyBase proxy);
+
+bool isError(mojom.Error err) {
+ return err != null && err.id != '';
+}
+
+class SyncbaseClient {
+ final mojom.SyncbaseProxy _proxy;
+
+ SyncbaseClient(ConnectToServiceFn cts, String url)
+ : _proxy = new mojom.SyncbaseProxy.unbound() {
+ print('connecting to $url');
+ cts(url, _proxy);
+ print('connected');
+ }
+
+ // Closes the connection to the syncbase server.
+ // TODO(nlacasse): Is this necessary?
+ Future close({bool immediate: false}) {
+ return _proxy.close(immediate: immediate);
+ }
+
+ // app returns the app with the given name, which should not contain slashes.
+ SyncbaseApp app(String name) => new SyncbaseApp._internal(_proxy, name);
+
+ Future<mojom.Perms> getPermissions() async {
+ var v = await _proxy.ptr.serviceGetPermissions();
+ if (isError(v.err)) throw v.err;
+ // TODO(nlacasse): We need to return the version too. Create a struct type
+ // that combines perms and version?
+ return v.perms;
+ }
+
+ Future setPermissions(mojom.Perms perms, String version) async {
+ var v = await _proxy.ptr.serviceSetPermissions(perms, version);
+ if (isError(v.err)) throw v.err;
+ }
+}
diff --git a/dart/pubspec.yaml b/dart/pubspec.yaml
new file mode 100644
index 0000000..bfef881
--- /dev/null
+++ b/dart/pubspec.yaml
@@ -0,0 +1,6 @@
+name: ether
+dependencies:
+ mojo: any
+dev_dependencies:
+ dart_style: any
+ test: any
diff --git a/dart/test/echo_test.dart b/dart/test/echo_test.dart
new file mode 100644
index 0000000..00bbf0c
--- /dev/null
+++ b/dart/test/echo_test.dart
@@ -0,0 +1,48 @@
+#!mojo mojo:dart_content_handler
+import 'dart:async';
+
+import 'package:mojo/core.dart' show MojoHandle;
+import 'package:test/test.dart';
+
+import 'package:ether/echo_client.dart' show EchoClient;
+import 'package:ether/initialized_application.dart' show InitializedApplication;
+
+main(List args) async {
+ InitializedApplication app = new InitializedApplication.fromHandle(args[0]);
+ await app.initialized;
+
+ EchoClient c = new EchoClient(
+ app.connectToService, 'https://mojo.v.io/echo_server.mojo');
+
+ tearDown(() {
+ app.resetConnections();
+ });
+
+ test('echo string returns correct response', () {
+ String input = 'foobee';
+ Future<String> got = c.echo(input);
+ expect(got, completion(equals(input)));
+ });
+
+ test('echo empty string returns correct response', () {
+ String input = '';
+ Future<String> got = c.echo(input);
+ expect(got, completion(equals(input)));
+ });
+
+ // Append a final test to terminate shell connection.
+ // TODO(nlacasse): Remove this once package 'test' supports a global tearDown
+ // callback. See https://github.com/dart-lang/test/issues/18.
+ test('terminate shell connection', () async {
+ await c.close();
+ expect(MojoHandle.reportLeakedHandles(), isTrue);
+
+ // TODO(nlacasse): When running mojo_shell with --enable-multiprocess,
+ // closing the application causes a non-graceful shutdown. To avoid this,
+ // we sleep for a second so the test reporter can finish and print the
+ // results before we close the app. Once mojo_shell can shut down more
+ // gracefully, we should call app.close directly in the test and not in
+ // this Timer.
+ new Timer(new Duration(seconds: 1), app.close);
+ });
+}
diff --git a/dart/test/syncbase_app_test.dart b/dart/test/syncbase_app_test.dart
new file mode 100644
index 0000000..97ef1e5
--- /dev/null
+++ b/dart/test/syncbase_app_test.dart
@@ -0,0 +1,29 @@
+library syncbase_app_test;
+
+import 'package:test/test.dart';
+
+import 'package:ether/syncbase_client.dart' show SyncbaseClient;
+
+import './utils.dart' as utils;
+
+runAppTests(SyncbaseClient c) {
+ test('getting a handle to an app', () {
+ var appName = utils.uniqueName('app');
+ var app = c.app(appName);
+ expect(app.relativeName, equals(appName));
+ expect(app.fullName, equals(appName));
+ });
+
+ test('creating and deleting an app', () async {
+ var appName = utils.uniqueName('app');
+ var app = c.app(appName);
+
+ expect(await app.exists(), equals(false));
+ await app.create(utils.emptyPerms());
+ expect(await app.exists(), equals(true));
+ await app.delete();
+ expect(await app.exists(), equals(false));
+ });
+
+ // TODO(nlacasse): Test app.get/setPermissions.
+}
diff --git a/dart/test/syncbase_database_test.dart b/dart/test/syncbase_database_test.dart
new file mode 100644
index 0000000..dd81c0f
--- /dev/null
+++ b/dart/test/syncbase_database_test.dart
@@ -0,0 +1,32 @@
+library syncbase_database_test;
+
+import 'package:test/test.dart';
+
+import 'package:ether/syncbase_client.dart' show SyncbaseClient;
+
+import './utils.dart' as utils;
+
+runDatabaseTests(SyncbaseClient c) {
+ test('getting a handle to a database', () {
+ var app = c.app(utils.uniqueName('app'));
+ var dbName = utils.uniqueName('db');
+ var db = app.noSqlDatabase(dbName);
+ expect(db.relativeName, equals(dbName));
+ expect(db.fullName, equals(app.fullName + '/' + dbName));
+ });
+
+ test('creating and deleting a database', () async {
+ var app = c.app(utils.uniqueName('app'));
+ await app.create(utils.emptyPerms());
+
+ var db = app.noSqlDatabase(utils.uniqueName('db'));
+
+ expect(await db.exists(), equals(false));
+ await db.create(utils.emptyPerms());
+ expect(await db.exists(), equals(true));
+ await db.delete();
+ expect(await db.exists(), equals(false));
+ });
+
+ // TODO(nlacasse): Test database.get/setPermissions.
+}
diff --git a/dart/test/syncbase_row_test.dart b/dart/test/syncbase_row_test.dart
new file mode 100644
index 0000000..1ce6643
--- /dev/null
+++ b/dart/test/syncbase_row_test.dart
@@ -0,0 +1,51 @@
+library syncbase_row_test;
+
+import 'dart:convert' show UTF8;
+
+import 'package:test/test.dart';
+
+import 'package:ether/syncbase_client.dart' show SyncbaseClient;
+
+import './utils.dart' as utils;
+
+runRowTests(SyncbaseClient c) {
+ test('getting a handle to a row', () {
+ var app = c.app(utils.uniqueName('app'));
+ var db = app.noSqlDatabase(utils.uniqueName('db'));
+ var table = db.table(utils.uniqueName('table'));
+
+ var rowName = utils.uniqueName('row');
+ var row = table.row(rowName);
+
+ expect(row.relativeName, equals(rowName));
+ expect(row.fullName, equals(table.fullName + '/' + rowName));
+ });
+
+ test('putting and getting a row', () async {
+ var app = c.app(utils.uniqueName('app'));
+ await app.create(utils.emptyPerms());
+ var db = app.noSqlDatabase(utils.uniqueName('db'));
+ await db.create(utils.emptyPerms());
+ var table = db.table(utils.uniqueName('table'));
+ await table.create(utils.emptyPerms());
+
+ var row = table.row(utils.uniqueName('row'));
+
+ expect(await row.exists(), equals(false));
+
+ var value1 = UTF8.encode("foo");
+ await row.put(value1);
+
+ expect(await row.exists(), equals(true));
+ expect(await row.get(), equals(value1));
+
+ var value2 = UTF8.encode("bar");
+ await row.put(value2);
+
+ expect(await row.exists(), equals(true));
+ expect(await row.get(), equals(value2));
+
+ await row.delete();
+ expect(await row.exists(), equals(false));
+ });
+}
diff --git a/dart/test/syncbase_table_test.dart b/dart/test/syncbase_table_test.dart
new file mode 100644
index 0000000..7aea3fe
--- /dev/null
+++ b/dart/test/syncbase_table_test.dart
@@ -0,0 +1,63 @@
+library syncbase_table_test;
+
+import 'dart:convert' show UTF8;
+
+import 'package:test/test.dart';
+
+import 'package:ether/syncbase_client.dart' show SyncbaseClient;
+
+import './utils.dart' as utils;
+
+runTableTests(SyncbaseClient c) {
+ test('getting a handle to a table', () {
+ var app = c.app(utils.uniqueName('app'));
+ var db = app.noSqlDatabase(utils.uniqueName('db'));
+ var tableName = utils.uniqueName('table');
+ var table = db.table(tableName);
+ expect(table.relativeName, equals(tableName));
+ expect(table.fullName, equals(db.fullName + '/' + tableName));
+ });
+
+ test('creating and deleting a table', () async {
+ var app = c.app(utils.uniqueName('app'));
+ await app.create(utils.emptyPerms());
+ var db = app.noSqlDatabase(utils.uniqueName('db'));
+ await db.create(utils.emptyPerms());
+
+ var table = db.table(utils.uniqueName('table'));
+
+ expect(await table.exists(), equals(false));
+ await table.create(utils.emptyPerms());
+ expect(await table.exists(), equals(true));
+ await table.delete();
+ expect(await table.exists(), equals(false));
+ });
+
+ test('scanning rows', () async {
+ var app = c.app(utils.uniqueName('app'));
+ await app.create(utils.emptyPerms());
+ var db = app.noSqlDatabase(utils.uniqueName('db'));
+ await db.create(utils.emptyPerms());
+ var table = db.table(utils.uniqueName('table'));
+ await table.create(utils.emptyPerms());
+
+ // Put some rows.
+ var rowNames = [utils.uniqueName('rowA'), utils.uniqueName('rowB')];
+
+ for (var rowName in rowNames) {
+ var row = table.row(rowName);
+ await row.put(UTF8.encode(utils.uniqueName('value')));
+ }
+
+ // Scan!
+ var stream = table.scan(UTF8.encode(''), UTF8.encode('z'));
+
+ var gotRows = await stream.toList();
+ expect(gotRows, hasLength(rowNames.length));
+ expect(gotRows[0].key, equals(rowNames[0]));
+ expect(gotRows[1].key, equals(rowNames[1]));
+
+ // TODO(nlacasse): Write tests that check that 'start' and 'limit' are
+ // working properly.
+ });
+}
diff --git a/dart/test/syncbase_test.dart b/dart/test/syncbase_test.dart
new file mode 100755
index 0000000..f55ae4a
--- /dev/null
+++ b/dart/test/syncbase_test.dart
@@ -0,0 +1,48 @@
+#!mojo mojo:dart_content_handler
+import 'dart:async';
+
+import 'package:mojo/core.dart' show MojoHandle;
+import 'package:test/test.dart';
+
+import 'package:ether/initialized_application.dart' show InitializedApplication;
+import 'package:ether/syncbase_client.dart' show SyncbaseClient;
+
+// Import other test files.
+import './syncbase_app_test.dart' show runAppTests;
+import './syncbase_database_test.dart' show runDatabaseTests;
+import './syncbase_row_test.dart' show runRowTests;
+import './syncbase_table_test.dart' show runTableTests;
+
+main(List args) async {
+ InitializedApplication app = new InitializedApplication.fromHandle(args[0]);
+ await app.initialized;
+
+ SyncbaseClient c = new SyncbaseClient(
+ app.connectToService, 'https://mojo.v.io/syncbase_server.mojo');
+
+ tearDown(() {
+ app.resetConnections();
+ });
+
+ // Run imported tests.
+ runAppTests(c);
+ runDatabaseTests(c);
+ runTableTests(c);
+ runRowTests(c);
+
+ // Append a final test to terminate shell connection.
+ // TODO(nlacasse): Remove this once package 'test' supports a global tearDown
+ // callback. See https://github.com/dart-lang/test/issues/18.
+ test('terminate shell connection', () async {
+ await c.close();
+ expect(MojoHandle.reportLeakedHandles(), isTrue);
+
+ // TODO(nlacasse): When running mojo_shell with --enable-multiprocess,
+ // closing the application causes a non-graceful shutdown. To avoid this,
+ // we sleep for a second so the test reporter can finish and print the
+ // results before we close the app. Once mojo_shell can shut down more
+ // gracefully, we should call app.close directly in the test and not in
+ // this Timer.
+ new Timer(new Duration(seconds: 1), app.close);
+ });
+}
diff --git a/dart/test/utils.dart b/dart/test/utils.dart
new file mode 100644
index 0000000..37c4e8a
--- /dev/null
+++ b/dart/test/utils.dart
@@ -0,0 +1,18 @@
+library utils;
+
+import 'package:ether/syncbase_client.dart' show Perms;
+
+// Returns an empty Perms object.
+Perms emptyPerms() => new Perms()..json = '{}';
+
+// Returns the current timestamp in ms since epoch.
+int timestamp() => new DateTime.now().millisecondsSinceEpoch;
+
+int _nameCounter = timestamp();
+
+// Returns a new unique name.
+String uniqueName(String type) {
+ type ??= 'unknown';
+ _nameCounter++;
+ return type + '-' + _nameCounter.toString();
+}
diff --git a/go/src/echo_server.go b/go/src/echo_server.go
new file mode 100644
index 0000000..f07e438
--- /dev/null
+++ b/go/src/echo_server.go
@@ -0,0 +1,69 @@
+// Copyright 2015 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// NOTE(nlacasse): This file was taken from $MOJO_DIR/src/examples/go. The
+// only changes are the mojom import path.
+
+package main
+
+import (
+ "log"
+
+ "mojo/public/go/application"
+ "mojo/public/go/bindings"
+ "mojo/public/go/system"
+
+ "mojom/echo"
+)
+
+//#include "mojo/public/c/system/types.h"
+import "C"
+
+type echoImpl struct {
+}
+
+func (e *echoImpl) EchoString(in *string) (out *string, err error) {
+ log.Printf("server: %s\n", *in)
+ return in, nil
+}
+
+type delegate struct {
+ stubs []*bindings.Stub
+}
+
+func (d *delegate) Initialize(ctx application.Context) {}
+
+func (d *delegate) Create(req echo.Echo_Request) {
+ stub := echo.NewEchoStub(req, &echoImpl{}, bindings.GetAsyncWaiter())
+ d.stubs = append(d.stubs, stub)
+ go func() {
+ for {
+ if err := stub.ServeRequest(); err != nil {
+ connErr, ok := err.(*bindings.ConnectionError)
+ if !ok || !connErr.Closed() {
+ log.Println(err)
+ }
+ break
+ }
+ }
+ }()
+}
+
+func (d *delegate) AcceptConnection(conn *application.Connection) {
+ conn.ProvideServices(&echo.Echo_ServiceFactory{d})
+}
+
+func (d *delegate) Quit() {
+ for _, stub := range d.stubs {
+ stub.Close()
+ }
+}
+
+//export MojoMain
+func MojoMain(handle C.MojoHandle) C.MojoResult {
+ application.Run(&delegate{}, system.MojoHandle(handle))
+ return C.MOJO_RESULT_OK
+}
+
+func main() {}
diff --git a/mojoconfig b/mojoconfig
new file mode 100644
index 0000000..8fb6c9c
--- /dev/null
+++ b/mojoconfig
@@ -0,0 +1,23 @@
+# Derived from $MOJO_DIR/mojoconfig.
+
+{
+ 'dev_servers': [
+ {
+ 'host': 'https://mojo.v.io/',
+ 'mappings': [
+ ('', [
+ # For echo_server.mojo and syncbase_server.mojo.
+ '@{ETHER_BUILD_DIR}',
+ # For echo_example.dart and syncbase_example.dart.
+ '@{ETHER_DIR}/dart/bin'
+ ]),
+ ],
+ },
+ {
+ 'host': 'https://test.mojo.v.io/',
+ 'mappings': [
+ ('', ['@{ETHER_DIR}/dart/test']),
+ ],
+ },
+ ],
+}
diff --git a/mojom/echo.mojom b/mojom/echo.mojom
new file mode 100644
index 0000000..14ca169
--- /dev/null
+++ b/mojom/echo.mojom
@@ -0,0 +1,9 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+module mojo;
+
+interface Echo {
+ EchoString(string? value) => (string? value);
+};
diff --git a/mojom/syncbase.mojom b/mojom/syncbase.mojom
new file mode 100644
index 0000000..00a22c5
--- /dev/null
+++ b/mojom/syncbase.mojom
@@ -0,0 +1,143 @@
+// TODO(sadovsky): Copy struct and interface comments from VDL files.
+
+// TODO(sadovsky): This should probably be something else.
+module mojo;
+
+// TODO(sadovsky): Put structs in a separate types.mojom file.
+// TODO(sadovsky): Loose representation of verror. Improve this.
+struct Error {
+ string id; // empty string means no error
+ uint32 action_code;
+ string msg;
+};
+
+// TODO(sadovsky): Decide how to represent perms.
+struct Perms {
+ string json;
+};
+
+struct BatchOptions {
+ string hint;
+ bool read_only;
+};
+
+struct PrefixPerms {
+ string prefix;
+ Perms perms;
+};
+
+struct SyncGroupSpec {
+ string description;
+ Perms perms;
+ array<string> prefixes;
+ array<string> mount_tables;
+ bool is_private;
+};
+
+struct SyncGroupMemberInfo {
+ uint8 sync_priority;
+};
+
+struct Result {
+ array<array<uint8>> values;
+};
+
+interface ExecStream {
+ OnResult(Result result);
+ OnDone(Error err);
+};
+
+struct KeyValue {
+ string key;
+ array<uint8> value;
+};
+
+interface ScanStream {
+ OnKeyValue(KeyValue key_value);
+ OnDone(Error err);
+};
+
+// TODO(sadovsky): Add schema version to all RPCs. See v.io/c/13734.
+// All 'name' params are service-relative object names.
+
+// Error handling modeled after:
+// https://github.com/domokit/mojo/blob/master/mojo/services/files/public/interfaces/file.mojom
+interface Syncbase {
+ ////////////////////////////////////////
+ // Service
+
+ ServiceGetPermissions() => (Error err, Perms perms, string version);
+ ServiceSetPermissions(Perms perms, string version) => (Error err);
+
+ ////////////////////////////////////////
+ // App
+
+ AppCreate(string name, Perms perms) => (Error err);
+ AppDelete(string name) => (Error err);
+ AppExists(string name) => (Error err, bool exists);
+ AppGetPermissions(string name) => (Error err, Perms perms, string version);
+ AppSetPermissions(string name, Perms perms, string version) => (Error err);
+
+ ////////////////////////////////////////
+ // nosql.Database
+
+ // TODO(sadovsky): Add SchemaMetadata argument.
+ DbCreate(string name, Perms perms) => (Error err);
+ DbDelete(string name) => (Error err);
+ DbExists(string name) => (Error err, bool exists);
+ DbExec(string name, string query, ExecStream stream) => (Error err);
+ DbBeginBatch(string name, BatchOptions? bo) => (Error err, string batch_dn);
+ DbCommit(string name) => (Error err);
+ DbAbort(string name) => (Error err);
+ DbGetPermissions(string name) => (Error err, Perms perms, string version);
+ DbSetPermissions(string name, Perms perms, string version) => (Error err);
+
+ // TODO(sadovsky): Add DatabaseWatcher, BlobManager, and SchemaManager
+ // methods.
+
+ ////////////////////////////////////////
+ // nosql.Database:SyncGroupManager
+
+ DbGetSyncGroupNames(string name) => (Error err, array<string> names);
+ DbCreateSyncGroup(
+ string name, string sg_name, SyncGroupSpec spec,
+ SyncGroupMemberInfo my_info)
+ => (Error err);
+ DbJoinSyncGroup(string name, string sg_name, SyncGroupMemberInfo my_info)
+ => (Error err);
+ DbLeaveSyncGroup(string name, string sg_name) => (Error err);
+ DbDestroySyncGroup(string name, string sg_name) => (Error err);
+ DbEjectFromSyncGroup(string name, string sg_name, string member)
+ => (Error err);
+ DbGetSyncGroupSpec(string name, string sg_name)
+ => (Error err, SyncGroupSpec spec, string version);
+ DbSetSyncGroupSpec(
+ string name, string sg_name, SyncGroupSpec spec, string version)
+ => (Error err);
+ DbGetSyncGroupMembers(string name, string sg_name)
+ => (Error err, map<string, SyncGroupMemberInfo> infos);
+
+ ////////////////////////////////////////
+ // nosql.Table
+
+ TableCreate(string name, Perms perms) => (Error err);
+ TableDelete(string name) => (Error err);
+ TableExists(string name) => (Error err, bool exists);
+ TableDeleteRowRange(string name, array<uint8> start, array<uint8> limit)
+ => (Error err);
+ TableScan(
+ string name, array<uint8> start, array<uint8> limit, ScanStream stream)
+ => (Error err);
+ TableGetPermissions(string name, string key)
+ => (Error err, array<PrefixPerms> perms_arr);
+ TableSetPermissions(string name, string prefix, Perms perms) => (Error err);
+ TableDeletePermissions(string name, string prefix) => (Error err);
+
+ ////////////////////////////////////////
+ // nosql.Row
+
+ RowExists(string name) => (Error err, bool exists);
+ RowGet(string name) => (Error err, array<uint8> value);
+ RowPut(string name, array<uint8> value) => (Error err);
+ RowDelete(string name) => (Error err);
+};
diff --git a/sky_demo/lib/main.dart b/sky_demo/lib/main.dart
new file mode 100644
index 0000000..003f819
--- /dev/null
+++ b/sky_demo/lib/main.dart
@@ -0,0 +1,113 @@
+import 'dart:async';
+import 'dart:convert' show UTF8;
+
+import 'package:sky/mojo/embedder.dart' show embedder;
+import 'package:sky/widgets.dart';
+
+import 'package:ether/echo_client.dart' show EchoClient;
+import 'package:ether/syncbase_client.dart'
+ show Perms, SyncbaseClient, SyncbaseTable;
+
+log(String msg) {
+ DateTime now = new DateTime.now();
+ print('$now $msg');
+}
+
+Perms emptyPerms() => new Perms()..json = '{}';
+
+class DemoApp extends App {
+ final EchoClient _echoClient;
+ final SyncbaseClient _syncbaseClient;
+
+ DemoApp()
+ : _echoClient = new EchoClient(
+ embedder.connectToService, 'https://mojo.v.io/echo_server.mojo'),
+ _syncbaseClient = new SyncbaseClient(embedder.connectToService,
+ 'https://mojo.v.io/syncbase_server.mojo');
+
+ int seq = 0;
+ SyncbaseTable tb;
+ String sendMsg, recvMsg, putStr, getStr;
+
+ Future doEcho() async {
+ log('DemoApp.doEcho');
+
+ setState(() {
+ sendMsg = seq.toString();
+ recvMsg = '';
+ });
+ seq++;
+ log('setState sendMsg done');
+
+ String recvMsgAsync = await _echoClient.echo(sendMsg);
+
+ setState(() {
+ recvMsg = recvMsgAsync;
+ });
+ log('setState recvMsg done');
+ }
+
+ Future doSyncbaseInit() async {
+ log('DemoApp.doSyncbaseInit');
+ if (tb != null) {
+ log('syncbase already initialized');
+ return;
+ }
+ // TODO(sadovsky): Handle "already exists" case.
+ var app = _syncbaseClient.app('app');
+ await app.create(emptyPerms());
+ var db = app.noSqlDatabase('db');
+ await db.create(emptyPerms());
+ var table = db.table('table');
+ await table.create(emptyPerms());
+ tb = table;
+ log('syncbase is now initialized');
+ }
+
+ Future doPutGet() async {
+ log('DemoApp.doPutGet');
+ await doSyncbaseInit();
+
+ setState(() {
+ putStr = seq.toString();
+ getStr = '';
+ });
+ seq++;
+ log('setState putStr done');
+
+ // TODO(sadovsky): Switch to tb.put/get once they exist.
+ var row = tb.row('key');
+ await row.put(UTF8.encode(putStr));
+ var getBytes = await row.get();
+
+ setState(() {
+ getStr = UTF8.decode(getBytes);
+ });
+ log('setState getStr done');
+ }
+
+ // TODO(sadovsky): I don't think Sky calls App.close().
+ Future close({bool immediate: false}) async {
+ log('DemoApp.close');
+ await _echoClient.close(immediate: immediate);
+ await _syncbaseClient.close(immediate: immediate);
+ }
+
+ Widget build() {
+ return new Container(
+ decoration:
+ const BoxDecoration(backgroundColor: const Color(0xFF00ACC1)),
+ child: new Flex([
+ new RaisedButton(child: new Text('doEcho'), onPressed: doEcho),
+ new Text('sendMsg: $sendMsg'),
+ new Text('recvMsg: $recvMsg'),
+ new RaisedButton(child: new Text('doPutGet'), onPressed: doPutGet),
+ new Text('putStr: $putStr'),
+ new Text('getStr: $getStr')
+ ], direction: FlexDirection.vertical));
+ }
+}
+
+void main() {
+ runApp(new DemoApp());
+}
diff --git a/sky_demo/mojoconfig b/sky_demo/mojoconfig
new file mode 100644
index 0000000..ab18775
--- /dev/null
+++ b/sky_demo/mojoconfig
@@ -0,0 +1,34 @@
+# Combination of ../mojoconfig and $MOJO_DIR/src/mojo/tools/configs/sky.
+
+{
+ 'dev_servers': [
+ {
+ 'host': 'https://mojo.v.io/',
+ 'mappings': [
+ ('packages/', [
+ # For sky_demo packages.
+ '@{ETHER_DIR}/sky_demo/packages',
+ ]),
+ ('', [
+ # For echo_server.mojo and syncbase_server.mojo.
+ '@{ETHER_BUILD_DIR}',
+ # For sky_demo/lib/main.dart.
+ '@{ETHER_DIR}',
+ ]),
+ ],
+ },
+ {
+ 'host': 'https://sky/',
+ 'mappings': [
+ ('', [
+ # For sky_viewer.mojo.
+ '@{SKY_BUILD_DIR}'
+ ]),
+ ],
+ }
+ ],
+
+ 'content_handlers': {
+ 'application/dart': 'https://sky/sky_viewer.mojo',
+ }
+}
diff --git a/sky_demo/pubspec.yaml b/sky_demo/pubspec.yaml
new file mode 100644
index 0000000..ca5894e
--- /dev/null
+++ b/sky_demo/pubspec.yaml
@@ -0,0 +1,8 @@
+name: sky_demo
+dependencies:
+ sky: any
+ sky_tools: any
+ ether: any
+dependency_overrides:
+ ether:
+ path: ../dart
diff --git a/tests b/tests
new file mode 100644
index 0000000..03a494f
--- /dev/null
+++ b/tests
@@ -0,0 +1,14 @@
+# See $MOJO_DIR/src/mojo/devtools/common/mojo_test for the format of this file.
+
+tests = [
+ {
+ "test": "https://test.mojo.v.io/echo_test.dart",
+ "type": "dart",
+ "timeout": 10
+ },
+ {
+ "test": "https://test.mojo.v.io/syncbase_test.dart",
+ "type": "dart",
+ "timeout": 30
+ }
+]
diff --git a/tools/sync_repos.sh b/tools/sync_repos.sh
new file mode 100755
index 0000000..51c69b1
--- /dev/null
+++ b/tools/sync_repos.sh
@@ -0,0 +1,36 @@
+#!/usr/bin/env bash
+
+set -euo pipefail
+
+DESKTOP="${DESKTOP:-1}"
+ANDROID="${ANDROID:-1}"
+
+####################
+# Mojo
+
+cd $MOJO_DIR/src
+git pull
+gclient sync
+if [[ "${DESKTOP}" = "1" ]]; then
+ ./mojo/tools/mojob.py gn
+ ./mojo/tools/mojob.py build
+fi
+if [[ "${ANDROID}" = "1" ]]; then
+ ./mojo/tools/mojob.py gn --android
+ ./mojo/tools/mojob.py build --android
+fi
+
+####################
+# Sky
+
+cd $SKY_DIR/src
+git pull
+gclient sync
+if [[ "${DESKTOP}" = "1" ]]; then
+ ./sky/tools/gn
+ ninja -C out/Debug
+fi
+if [[ "${ANDROID}" = "1" ]]; then
+ ./sky/tools/gn --android
+ ninja -C out/android_Debug
+fi