Sync Session API (#825)

* Add Session class and expose it to JS

* Add error property on the sync config object for the event handler

* tests

* refactor access token refresh

* chrome debugging
This commit is contained in:
Yavor Georgiev 2017-02-01 14:18:59 +01:00 committed by GitHub
parent 408f5588f8
commit 9d1d970b1f
12 changed files with 365 additions and 58 deletions

View File

@ -40,6 +40,7 @@ export const propTypes = {};
'REALM',
'RESULTS',
'USER',
'SESSION',
'UNDEFINED',
].forEach(function(type) {
Object.defineProperty(objectTypes, type, {

View File

@ -25,6 +25,7 @@ import List, { createList } from './lists';
import Results, { createResults } from './results';
import RealmObject, * as objects from './objects';
import User, { createUser } from './user';
import Session, { createSession } from './session';
import * as rpc from './rpc';
import * as util from './util';
import { static as staticUserMethods } from '../user-methods';
@ -36,6 +37,7 @@ rpc.registerTypeConverter(objectTypes.RESULTS, createResults);
rpc.registerTypeConverter(objectTypes.OBJECT, objects.createObject);
rpc.registerTypeConverter(objectTypes.REALM, createRealm);
rpc.registerTypeConverter(objectTypes.USER, createUser);
rpc.registerTypeConverter(objectTypes.SESSION, createSession);
function createRealm(_, info) {
let realm = Object.create(Realm.prototype);
@ -54,6 +56,7 @@ function setupRealm(realm, realmId) {
'readOnly',
'schema',
'schemaVersion',
'syncSession',
].forEach((name) => {
Object.defineProperty(realm, name, {get: util.getterForProperty(name)});
});
@ -131,7 +134,8 @@ util.createMethods(Realm.prototype, objectTypes.REALM, [
], true);
const Sync = {
User
User,
Session
};
Object.defineProperties(Realm, {

48
lib/browser/session.js Normal file
View File

@ -0,0 +1,48 @@
////////////////////////////////////////////////////////////////////////////
//
// Copyright 2016 Realm Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
////////////////////////////////////////////////////////////////////////////
'use strict';
import { keys, objectTypes } from './constants';
import { getterForProperty, createMethods } from './util';
export default class Session { }
Object.defineProperties(Session.prototype, {
url: getterForProperty('url'),
state: getterForProperty('state')
});
createMethods(Session.prototype, objectTypes.SESSION, [
'_refreshAccessToken',
'_simulateError'
]);
export function createSession(realmId, info) {
let sessionProxy = Object.create(Session.prototype);
// FIXME: This is currently necessary because util/createMethod expects
// the realm id to be present on any object that is used over rpc
sessionProxy[keys.realm] = "(Session object)";
sessionProxy[keys.id] = info.id;
sessionProxy[keys.type] = objectTypes.SESSION;
Object.assign(sessionProxy, info.data);
return sessionProxy;
}

View File

@ -34,7 +34,8 @@ export default class User {
}
createMethods(User.prototype, objectTypes.USER, [
'logout'
'logout',
'_sessionForOnDiskPath'
]);
export function createUser(realmId, info) {

View File

@ -40,35 +40,31 @@ function auth_url(server) {
return server + 'auth';
}
function authenticateRealm(user, fileUrl, realmUrl, callback) {
var url = auth_url(user.server);
var options = {
function authenticateRealm(user, fileUrl, realmUrl) {
let parsedRealmUrl = url_parse(realmUrl);
const url = auth_url(user.server);
const options = {
method: 'POST',
body: JSON.stringify({
data: user.token,
path: url_parse(realmUrl).pathname,
path: parsedRealmUrl.pathname,
provider: 'realm',
app_id: ''
}),
headers: postHeaders
};
performFetch(url, options, function(error, response, body) {
if (error) {
callback(error);
}
else if (response.statusCode != 200) {
callback(new AuthError('Bad response: ' + response.statusCode));
}
else {
// TODO: validate JSON
callback(undefined, {
token: body.access_token.token,
file_url: url_parse(fileUrl).pathname,
resolved_realm_url: 'realm://' + url_parse(realmUrl).host + body.access_token.token_data.path
});
}
});
performFetch(url, options)
.then((response) => {
if (response.status != 200) {
//FIXME: propagate error to session error handler
} else {
return response.json().then((body) => {
parsedRealmUrl.set('pathname', body.access_token.token_data.path);
let session = user._sessionForOnDiskPath(fileUrl);
session._refreshAccessToken(body.access_token.token, parsedRealmUrl.href);
});
}
});
}
function _authenticate(userConstructor, server, json, callback) {

View File

@ -6,9 +6,8 @@ set -eo pipefail
. dependencies.list
if [ -f object-server-for-testing/node_modules/realm-object-server/CHANGELOG.md ]; then
current_version=$(head -n1 object-server-for-testing/node_modules/realm-object-server/CHANGELOG.md | cut -d" " -f2)
if [ "$REALM_OBJECT_SERVER_VERSION" = "$current_version" ]; then
if [ -f object-server-for-testing/node_modules/realm-object-server/package.json ]; then
if grep -q "\"version\": \"$REALM_OBJECT_SERVER_VERSION\"" object-server-for-testing/node_modules/realm-object-server/package.json; then
echo -e "yes\n" | object-server-for-testing/reset-server-realms.command
exit
fi

View File

@ -167,6 +167,9 @@ public:
static void get_schema_version(ContextType, ObjectType, ReturnValue &);
static void get_schema(ContextType, ObjectType, ReturnValue &);
static void get_read_only(ContextType, ObjectType, ReturnValue &);
#if REALM_ENABLE_SYNC
static void get_sync_session(ContextType, ObjectType, ReturnValue &);
#endif
// static methods
static void constructor(ContextType, ObjectType, size_t, const ValueType[]);
@ -208,6 +211,9 @@ public:
{"schemaVersion", {wrap<get_schema_version>, nullptr}},
{"schema", {wrap<get_schema>, nullptr}},
{"readOnly", {wrap<get_read_only>, nullptr}},
#if REALM_ENABLE_SYNC
{"syncSession", {wrap<get_sync_session>, nullptr}},
#endif
};
private:
@ -498,6 +504,19 @@ void RealmClass<T>::get_read_only(ContextType ctx, ObjectType object, ReturnValu
return_value.set(get_internal<T, RealmClass<T>>(object)->get()->config().read_only());
}
#if REALM_ENABLE_SYNC
template<typename T>
void RealmClass<T>::get_sync_session(ContextType ctx, ObjectType object, ReturnValue &return_value) {
auto realm = *get_internal<T, RealmClass<T>>(object);
if (std::shared_ptr<SyncSession> session = SyncManager::shared().get_existing_active_session(realm->config().path)) {
return_value.set(create_object<T, SessionClass<T>>(ctx, new WeakSession(session)));
} else {
return_value.set_null();
}
}
#endif
template<typename T>
void RealmClass<T>::objects(ContextType ctx, ObjectType this_object, size_t argc, const ValueType arguments[], ReturnValue &return_value) {
validate_argument_count(argc, 1);

View File

@ -36,7 +36,8 @@
namespace realm {
namespace js {
using SharedUser = std::shared_ptr<realm::SyncUser>;
using SharedUser = std::shared_ptr<realm::SyncUser>;
using WeakSession = std::weak_ptr<realm::SyncSession>;
template<typename T>
class UserClass : public ClassDefinition<T, SharedUser> {
@ -84,9 +85,11 @@ public:
};
static void logout(ContextType, ObjectType, size_t, const ValueType[], ReturnValue &);
static void session_for_on_disk_path(ContextType, ObjectType, size_t, const ValueType[], ReturnValue &);
MethodMap<T> const methods = {
{"logout", wrap<logout>}
{"logout", wrap<logout>},
{"_sessionForOnDiskPath", wrap<session_for_on_disk_path>}
};
};
@ -162,6 +165,126 @@ void UserClass<T>::logout(ContextType ctx, ObjectType object, size_t, const Valu
get_internal<T, UserClass<T>>(object)->get()->log_out();
}
template<typename T>
class SessionClass : public ClassDefinition<T, WeakSession> {
using ContextType = typename T::Context;
using FunctionType = typename T::Function;
using ObjectType = typename T::Object;
using ValueType = typename T::Value;
using String = js::String<T>;
using Object = js::Object<T>;
using Value = js::Value<T>;
using ReturnValue = js::ReturnValue<T>;
public:
std::string const name = "Session";
static FunctionType create_constructor(ContextType);
static void get_config(ContextType, ObjectType, ReturnValue &);
static void get_user(ContextType, ObjectType, ReturnValue &);
static void get_url(ContextType, ObjectType, ReturnValue &);
static void get_state(ContextType, ObjectType, ReturnValue &);
static void simulate_error(ContextType, ObjectType, size_t, const ValueType[], ReturnValue &);
static void refresh_access_token(ContextType, ObjectType, size_t, const ValueType[], ReturnValue &);
PropertyMap<T> const properties = {
{"config", {wrap<get_config>, nullptr}},
{"user", {wrap<get_user>, nullptr}},
{"url", {wrap<get_url>, nullptr}},
{"state", {wrap<get_state>, nullptr}}
};
MethodMap<T> const methods = {
{"_simulateError", wrap<simulate_error>},
{"_refreshAccessToken", wrap<refresh_access_token>}
};
};
template<typename T>
void UserClass<T>::session_for_on_disk_path(ContextType ctx, ObjectType object, size_t argc, const ValueType arguments[], ReturnValue &return_value) {
auto user = *get_internal<T, UserClass<T>>(object);
if (auto session = user->session_for_on_disk_path(Value::validated_to_string(ctx, arguments[0]))) {
return_value.set(create_object<T, SessionClass<T>>(ctx, new WeakSession(session)));
} else {
return_value.set_undefined();
}
}
template<typename T>
void SessionClass<T>::get_config(ContextType ctx, ObjectType object, ReturnValue &return_value) {
if (auto session = get_internal<T, SessionClass<T>>(object)->lock()) {
ObjectType config = Object::create_empty(ctx);
Object::set_property(ctx, config, "user", create_object<T, UserClass<T>>(ctx, new SharedUser(session->config().user)));
Object::set_property(ctx, config, "url", Value::from_string(ctx, session->config().realm_url));
return_value.set(config);
} else {
return_value.set_undefined();
}
}
template<typename T>
void SessionClass<T>::get_user(ContextType ctx, ObjectType object, ReturnValue &return_value) {
if (auto session = get_internal<T, SessionClass<T>>(object)->lock()) {
return_value.set(create_object<T, UserClass<T>>(ctx, new SharedUser(session->config().user)));
} else {
return_value.set_undefined();
}
}
template<typename T>
void SessionClass<T>::get_url(ContextType ctx, ObjectType object, ReturnValue &return_value) {
if (auto session = get_internal<T, SessionClass<T>>(object)->lock()) {
if (util::Optional<std::string> url = session->full_realm_url()) {
return_value.set(*url);
return;
}
}
return_value.set_undefined();
}
template<typename T>
void SessionClass<T>::get_state(ContextType ctx, ObjectType object, ReturnValue &return_value) {
static const std::string invalid("invalid");
static const std::string inactive("inactive");
static const std::string active("active");
return_value.set(invalid);
if (auto session = get_internal<T, SessionClass<T>>(object)->lock()) {
if (session->state() == SyncSession::PublicState::Inactive) {
return_value.set(inactive);
} else if (session->state() != SyncSession::PublicState::Error) {
return_value.set(active);
}
}
}
template<typename T>
void SessionClass<T>::simulate_error(ContextType ctx, ObjectType object, size_t argc, const ValueType arguments[], ReturnValue &) {
validate_argument_count(argc, 2);
if (auto session = get_internal<T, SessionClass<T>>(object)->lock()) {
SyncError error;
error.error_code = std::error_code(Value::validated_to_number(ctx, arguments[0]), realm::sync::protocol_error_category());
error.message = Value::validated_to_string(ctx, arguments[1]);
SyncSession::OnlyForTesting::handle_error(*session, std::move(error));
}
}
template<typename T>
void SessionClass<T>::refresh_access_token(ContextType ctx, ObjectType object, size_t argc, const ValueType arguments[], ReturnValue &) {
validate_argument_count(argc, 2);
if (auto session = get_internal<T, SessionClass<T>>(object)->lock()) {
std::string access_token = Value::validated_to_string(ctx, arguments[0], "accessToken");
std::string realm_url = Value::validated_to_string(ctx, arguments[1], "realmUrl");
session->refresh_access_token(std::move(access_token), std::move(realm_url));
}
}
template<typename T>
class SyncClass : public ClassDefinition<T, void *> {
using GlobalContextType = typename T::GlobalContext;
@ -185,14 +308,12 @@ public:
static void set_verify_servers_ssl_certificate(ContextType, ObjectType, size_t, const ValueType[], ReturnValue &);
// private
static void refresh_access_token(ContextType, ObjectType, size_t, const ValueType[], ReturnValue &);
static void populate_sync_config(ContextType, ObjectType realm_constructor, ObjectType config_object, Realm::Config&);
// static properties
static void get_is_developer_edition(ContextType, ObjectType, ReturnValue &);
MethodMap<T> const static_methods = {
{"refreshAccessToken", wrap<refresh_access_token>},
{"setLogLevel", wrap<set_sync_log_level>},
{"setVerifyServersSslCertificate", wrap<set_verify_servers_ssl_certificate>}
};
@ -204,7 +325,8 @@ inline typename T::Function SyncClass<T>::create_constructor(ContextType ctx) {
PropertyAttributes attributes = ReadOnly | DontEnum | DontDelete;
Object::set_property(ctx, sync_constructor, "User", ObjectWrap<T, UserClass<T>>::create_constructor(ctx), attributes);
Object::set_property(ctx, sync_constructor, "Session", ObjectWrap<T, SessionClass<T>>::create_constructor(ctx), attributes);
// setup synced realmFile paths
ensure_directory_exists_for_file(default_realm_file_directory());
SyncManager::shared().configure_file_system(default_realm_file_directory(), SyncManager::MetadataMode::NoEncryption);
@ -233,28 +355,6 @@ void SyncClass<T>::set_verify_servers_ssl_certificate(ContextType ctx, ObjectTyp
realm::SyncManager::shared().set_client_should_validate_ssl(verify_servers_ssl_certificate);
}
template<typename T>
void SyncClass<T>::refresh_access_token(ContextType ctx, ObjectType this_object, size_t argc, const ValueType arguments[], ReturnValue &return_value) {
validate_argument_count(argc, 2);
static const String token_string = "token";
static const String file_url_string = "file_url";
static const String realm_url_string = "resolved_realm_url";
ObjectType json_arguments = Value::validated_to_object(ctx, arguments[1]);
std::string token = Object::validated_get_string(ctx, json_arguments, token_string);
std::string file_url = Object::validated_get_string(ctx, json_arguments, file_url_string);
std::string realm_url = Object::validated_get_string(ctx, json_arguments, realm_url_string);
if (auto session = SyncManager::shared().get_existing_active_session(file_url)) {
session->refresh_access_token(token, realm_url);
return_value.set(true);
}
else {
return_value.set(false);
}
}
template<typename T>
void SyncClass<T>::populate_sync_config(ContextType ctx, ObjectType realm_constructor, ObjectType config_object, Realm::Config& config)
{
@ -263,7 +363,6 @@ void SyncClass<T>::populate_sync_config(ContextType ctx, ObjectType realm_constr
auto sync_config_object = Value::validated_to_object(ctx, sync_config_value);
ObjectType sync_constructor = Object::validated_get_object(ctx, realm_constructor, std::string("Sync"));
Protected<ValueType> refresh(ctx, Object::validated_get_function(ctx, sync_constructor, std::string("refreshAccessToken")));
Protected<ObjectType> protected_sync(ctx, sync_constructor);
Protected<typename T::GlobalContext> protected_ctx(Context<T>::get_global_context(ctx));
@ -283,15 +382,41 @@ void SyncClass<T>::populate_sync_config(ContextType ctx, ObjectType realm_constr
ObjectType user_constructor = Object::validated_get_object(ctx, protected_sync, std::string("User"));
FunctionType authenticate = Object::validated_get_function(ctx, user_constructor, std::string("_authenticateRealm"));
ValueType arguments[4];
ValueType arguments[3];
arguments[0] = create_object<T, UserClass<T>>(ctx, new SharedUser(config.user));
arguments[1] = Value::from_string(protected_ctx, path.c_str());
arguments[2] = Value::from_string(protected_ctx, config.realm_url.c_str());
arguments[3] = refresh;
Function::call(protected_ctx, authenticate, 4, arguments);
Function::call(protected_ctx, authenticate, 3, arguments);
}
});
std::function<SyncSessionErrorHandler> error_handler;
ValueType error_func = Object::get_property(ctx, sync_config_object, "error");
if (!Value::is_undefined(ctx, error_func)) {
Protected<FunctionType> protected_error_func(ctx, Value::validated_to_function(ctx, error_func));
error_handler = EventLoopDispatcher<SyncSessionErrorHandler>([=](auto session, auto error) {
HANDLESCOPE
ObjectType error_object = Object::create_empty(protected_ctx);
Object::set_property(protected_ctx, error_object, "message", Value::from_string(protected_ctx, error.message));
Object::set_property(protected_ctx, error_object, "isFatal", Value::from_boolean(protected_ctx, error.is_fatal));
Object::set_property(protected_ctx, error_object, "category", Value::from_string(protected_ctx, error.error_code.category().name()));
Object::set_property(protected_ctx, error_object, "code", Value::from_number(protected_ctx, error.error_code.value()));
ObjectType user_info = Object::create_empty(protected_ctx);
for (auto& kvp : error.user_info) {
Object::set_property(protected_ctx, user_info, kvp.first, Value::from_string(protected_ctx, kvp.second));
}
Object::set_property(protected_ctx, error_object, "userInfo", user_info);
ValueType arguments[2];
arguments[0] = create_object<T, SessionClass<T>>(protected_ctx, new WeakSession(session));
arguments[1] = error_object;
Function::call(protected_ctx, protected_error_func, 2, arguments);
});
}
ObjectType user = Object::validated_get_object(ctx, sync_config_object, "user");
SharedUser shared_user = *get_internal<T, UserClass<T>>(user);
if (shared_user->state() != SyncUser::State::Active) {
@ -303,7 +428,7 @@ void SyncClass<T>::populate_sync_config(ContextType ctx, ObjectType realm_constr
// FIXME - use make_shared
config.sync_config = std::shared_ptr<SyncConfig>(new SyncConfig{shared_user, raw_realm_url,
SyncSessionStopPolicy::AfterChangesUploaded,
std::move(bind), [=](auto, SyncError) {}});
std::move(bind), std::move(error_handler)});
config.schema_mode = SchemaMode::Additive;
config.path = realm::SyncManager::shared().path_for_realm(shared_user->identity(), raw_realm_url);
}

View File

@ -43,6 +43,7 @@ static const char * const RealmObjectTypesObject = "object";
static const char * const RealmObjectTypesResults = "results";
static const char * const RealmObjectTypesRealm = "realm";
static const char * const RealmObjectTypesUser = "user";
static const char * const RealmObjectTypesSession = "session";
static const char * const RealmObjectTypesUndefined = "undefined";
static RPCServer*& get_rpc_server(JSGlobalContextRef ctx) {
@ -389,6 +390,17 @@ json RPCServer::serialize_json_value(JSValueRef js_value) {
{"data", user_dict}
};
}
else if (jsc::Object::is_instance<js::SessionClass<jsc::Types>>(m_context, js_object)) {
json session_dict {
{"user", serialize_json_value(jsc::Object::get_property(m_context, js_object, "user"))},
{"config", serialize_json_value(jsc::Object::get_property(m_context, js_object, "config"))}
};
return {
{"type", RealmObjectTypesSession},
{"id", store_object(js_object)},
{"data", session_dict}
};
}
else if (jsc::Value::is_array(m_context, js_object)) {
uint32_t length = jsc::Object::validated_get_length(m_context, js_object);
std::vector<json> array;

View File

@ -142,6 +142,12 @@ module.exports = {
}
},
assertNull: function(value, errorMessage) {
if (value !== null) {
throw new TestFailureError(errorMessage || `Value ${value} expected to be null`);
}
},
isNode: function() {
// eslint-disable-next-line no-undef
return typeof process == 'object' && Object.prototype.toString.call(process) == '[object process]';

View File

@ -33,6 +33,7 @@ var TESTS = {
// If sync is enabled, run the user tests
if (Realm.Sync) {
TESTS.UserTests = require('./user-tests');
TESTS.SessionTests = require('./session-tests');
}
function node_require(module) { return require(module); }

95
tests/js/session-tests.js Normal file
View File

@ -0,0 +1,95 @@
////////////////////////////////////////////////////////////////////////////
//
// Copyright 2016 Realm Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
////////////////////////////////////////////////////////////////////////////
/* eslint-env es6, node */
'use strict';
const Realm = require('realm');
const TestCase = require('./asserts');
function uuid() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
}
function promisifiedRegister(server, username, password) {
return new Promise((resolve, reject) => {
Realm.Sync.User.register(server, username, password, (error, user) => {
if (error) {
reject(error);
} else {
resolve(user);
}
});
});
}
function wait(delay) {
return new Promise((resolve, reject) => setTimeout(resolve, delay));
}
module.exports = {
testLocalRealmHasNoSession() {
let realm = new Realm();
TestCase.assertNull(realm.syncSession);
},
testProperties() {
return promisifiedRegister('http://localhost:9080', uuid(), 'password').then(user => {
let config = { sync: { user, url: 'realm://localhost:9080/~/myrealm' } };
let realm = new Realm(config);
let session = realm.syncSession;
TestCase.assertInstanceOf(session, Realm.Sync.Session);
TestCase.assertEqual(session.user.identity, user.identity);
TestCase.assertEqual(session.config.url, config.sync.url);
TestCase.assertEqual(session.config.user.identity, config.sync.user.identity);
TestCase.assertUndefined(session.url);
TestCase.assertEqual(session.state, 'active');
// give the session enough time to refresh its access token and bind itself
return wait(500).then(() => {
TestCase.assertEqual(session.url, `realm://localhost:9080/${user.identity}/myrealm`);
});
});
},
testErrorHandling() {
return promisifiedRegister('http://localhost:9080', uuid(), 'password').then(user => {
let errors = [];
let config = { sync: { user,
url: 'realm://localhost:9080/~/myrealm',
error: (sender, error) => errors.push([sender, error])
} };
let realm = new Realm(config);
let session = realm.syncSession;
session._simulateError(123, 'simulated error');
return wait(100).then(() => {
TestCase.assertArrayLength(errors, 1);
TestCase.assertEqual(errors[0][0].config.url, session.config.url);
TestCase.assertEqual(errors[0][1].message, 'simulated error');
TestCase.assertEqual(errors[0][1].code, 123);
});
});
}
}