From 9a73d9d2155af4259a062cf82ef44039895c22ad Mon Sep 17 00:00:00 2001 From: blagoev Date: Sat, 6 May 2017 15:19:19 +0300 Subject: [PATCH 01/15] Improve VS Code debug Allow debugging of session-tests and improve VS Code debug tasks. New VS Code task to download and start server for debugging New VS Code tasks to build only changed files and full rebuild --- .vscode/launch.json | 42 ++++++++++++++++++++++++++++ .vscode/tasks.json | 32 ++++++++++++++++----- package.json | 6 ++-- scripts/download_and_start_server.sh | 6 ++++ 4 files changed, 77 insertions(+), 9 deletions(-) create mode 100755 scripts/download_and_start_server.sh diff --git a/.vscode/launch.json b/.vscode/launch.json index 347e625d..09612ab2 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -4,6 +4,24 @@ // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 "version": "0.2.0", "configurations": [ + { + "type": "lldb", + "request": "launch", + "name": "Debug LLDB", + "program": "node", + "cwd": "${workspaceRoot}/tests", + "args": ["--debug-brk=5858", "${workspaceRoot}/tests/node_modules/jasmine/bin/jasmine.js", "spec/unit_tests.js"], + "stopOnEntry": true + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug LLDB only", + "program": "node", + "cwd": "${workspaceRoot}/tests", + "args": ["${workspaceRoot}/tests/node_modules/jasmine/bin/jasmine.js", "spec/unit_tests.js"], + "stopOnEntry": false + }, { "type": "node", "request": "launch", @@ -24,6 +42,30 @@ "args": [ "spec/unit_tests.js" ] + }, + { + "type": "node", + "request": "launch", + "name": "Download & Start Server", + "cwd": "${workspaceRoot}/scripts" + + }, + { + "type": "node", + "request": "attach", + "name": "Attach to Port", + "address": "localhost", + "port": 5858 + } + ], + "compounds": [ + { + "name": "Rebuild + Start Server + Debug", + "configurations": ["Download & Start Server", "Debug Node Unit Tests (rebuild)"] + }, + { + "name": "Debug LLDB + NodeJS", + "configurations": ["Debug LLDB", "Attach to Port"] } ] } \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json index a8a937e6..adb49f34 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -2,15 +2,33 @@ // See https://go.microsoft.com/fwlink/?LinkId=733558 // for the documentation about the tasks.json format "version": "0.1.0", - "command": "npm", - "isShellCommand": true, - "showOutput": "always", - "suppressTaskName": true, "tasks": [ { - "taskName": "rebuild-node-tests", - - "args": ["run", "prenode-tests"] + "taskName": "Rebuild Node Tests", + "command": "npm", + "isShellCommand": true, + "showOutput": "always", + "suppressTaskName": true, + "args": ["run", "rebuild-changes"], + "isBuildCommand": false + }, + { + "taskName": "Build Node Tests", + "command": "npm", + "isShellCommand": true, + "showOutput": "always", + "suppressTaskName": true, + "args": ["run", "build-changes"], + "isBuildCommand": true + }, + { + "taskName": "Download and Start Server", + "command": "${workspaceRoot}/scripts/download_and_start_server.sh", + "isShellCommand": true, + "showOutput": "always", + "echoCommand": true, + "isBackground": true, + "suppressTaskName": true } ] } \ No newline at end of file diff --git a/package.json b/package.json index 69634f05..67ab0484 100644 --- a/package.json +++ b/package.json @@ -48,10 +48,12 @@ "set-version": "scripts/set-version.sh", "get-core-version": "env-cmd ./dependencies.list node -p process.env.REALM_CORE_VERSION", "get-sync-version": "env-cmd ./dependencies.list node -p process.env.REALM_SYNC_VERSION", - "lint": "eslint", + "lint": "eslint", "test": "scripts/test.sh", "install": "node-pre-gyp install --fallback-to-build", - "prepublish": "node scripts/prepublish.js", + "build-changes": "node-pre-gyp build --fallback-to-build --debug --build-from-source", + "rebuild-changes": "node-pre-gyp install --fallback-to-build --debug --build-from-source && cd tests && npm install", + "prepublish": "echo prepublishing && node scripts/prepublish.js", "eslint": "npm install && npm run lint .", "license-check": "npm install && license-checker --exclude \"MIT,ISC,BSD,Apache-2.0,BSD-2-Clause,BSD-3-Clause,WTFPL,Unlicense,(MIT AND CC-BY-3.0)\" | node scripts/handle-license-check.js", "jsdoc:clean": "rimraf ./docs/output", diff --git a/scripts/download_and_start_server.sh b/scripts/download_and_start_server.sh new file mode 100755 index 00000000..40e3842c --- /dev/null +++ b/scripts/download_and_start_server.sh @@ -0,0 +1,6 @@ +#!/bin/bash + +set -o pipefail +set -e +echo $(pwd) +sh scripts/download-object-server.sh && sh object-server-for-testing/start-object-server.command -f && echo \"Server PID: $!\" \ No newline at end of file From 9c700053afee3c95c6a2952785e6642632021610 Mon Sep 17 00:00:00 2001 From: blagoev Date: Sat, 6 May 2017 15:33:06 +0300 Subject: [PATCH 02/15] Fix EventLoopDispatcher Fixes callback not called due to dispatcher freed when instance go out of scope. Preserves the EventLoopSignal if there are pending invocations --- src/event_loop_dispatcher.hpp | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/event_loop_dispatcher.hpp b/src/event_loop_dispatcher.hpp index 1d08e036..0e3ff749 100644 --- a/src/event_loop_dispatcher.hpp +++ b/src/event_loop_dispatcher.hpp @@ -45,13 +45,17 @@ template class EventLoopDispatcher { using Tuple = std::tuple::type...>; private: + + struct Callback; + struct State { public: - State(std::function func) : m_func(func) { } + State(std::function func) : m_func(func), m_signal(nullptr) { } const std::function m_func; std::queue m_invocations; std::mutex m_mutex; + std::shared_ptr> m_signal; }; const std::shared_ptr m_state; @@ -67,6 +71,7 @@ private: ::_apply_polyfill::apply(tuple, m_state->m_func); m_state->m_invocations.pop(); } + m_state->m_signal = nullptr; } }; const std::shared_ptr> m_signal; @@ -92,6 +97,7 @@ public: { std::unique_lock lock(m_state->m_mutex); + m_state->m_signal = m_signal; m_state->m_invocations.push(std::make_tuple(args...)); } m_signal->notify(); From 44432660eae01defa807837c58b37db9f9ce00fd Mon Sep 17 00:00:00 2001 From: blagoev Date: Sun, 7 May 2017 02:02:05 +0300 Subject: [PATCH 03/15] Add MakeCallback method abstraction Needed for node to kick next tick properly. JSC uses regular call --- src/js_types.hpp | 1 + src/jsc/jsc_function.hpp | 5 +++++ src/node/node_function.hpp | 13 +++++++++++++ 3 files changed, 19 insertions(+) diff --git a/src/js_types.hpp b/src/js_types.hpp index 8a5a10be..1c6a3aec 100644 --- a/src/js_types.hpp +++ b/src/js_types.hpp @@ -151,6 +151,7 @@ struct Function { using ObjectType = typename T::Object; using ValueType = typename T::Value; + static ValueType callback(ContextType, const FunctionType &, const ObjectType &, size_t, const ValueType[]); static ValueType call(ContextType, const FunctionType &, const ObjectType &, size_t, const ValueType[]); template static ValueType call(ContextType ctx, const FunctionType &function, const ObjectType &this_object, const ValueType (&arguments)[N]) diff --git a/src/jsc/jsc_function.hpp b/src/jsc/jsc_function.hpp index 866c973f..da3f2a3d 100644 --- a/src/jsc/jsc_function.hpp +++ b/src/jsc/jsc_function.hpp @@ -33,6 +33,11 @@ inline JSValueRef jsc::Function::call(JSContextRef ctx, const JSObjectRef &funct return result; } +template<> +inline JSValueRef jsc::Function::callback(JSContextRef ctx, const JSObjectRef &function, const JSObjectRef &this_object, size_t argc, const JSValueRef arguments[]) { + return jsc::Function::call(ctx, function, this_object, argc, arguments); +} + template<> inline JSObjectRef jsc::Function::construct(JSContextRef ctx, const JSObjectRef &function, size_t argc, const JSValueRef arguments[]) { JSValueRef exception = nullptr; diff --git a/src/node/node_function.hpp b/src/node/node_function.hpp index 39e90720..cb07959a 100644 --- a/src/node/node_function.hpp +++ b/src/node/node_function.hpp @@ -36,6 +36,19 @@ inline v8::Local node::Function::call(v8::Isolate* isolate, const v8: return result.ToLocalChecked(); } +template<> +inline v8::Local node::Function::callback(v8::Isolate* isolate, const v8::Local &function, const v8::Local &this_object, size_t argc, const v8::Local arguments[]) { + Nan::TryCatch trycatch; + + auto recv = this_object.IsEmpty() ? isolate->GetCurrentContext()->Global() : this_object; + auto result = Nan::MakeCallback(recv, function, (int)argc, const_cast*>(arguments)); + + if (trycatch.HasCaught()) { + throw node::Exception(isolate, trycatch.Exception()); + } + return result; +} + template<> inline v8::Local node::Function::construct(v8::Isolate* isolate, const v8::Local &function, size_t argc, const v8::Local arguments[]) { Nan::TryCatch trycatch; From 5a6baef61206ef27e6e514a5d0441993745b5332 Mon Sep 17 00:00:00 2001 From: blagoev Date: Sun, 7 May 2017 02:02:57 +0300 Subject: [PATCH 04/15] Fix event_loop_dispatcher --- src/event_loop_dispatcher.hpp | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/event_loop_dispatcher.hpp b/src/event_loop_dispatcher.hpp index 0e3ff749..df454fd6 100644 --- a/src/event_loop_dispatcher.hpp +++ b/src/event_loop_dispatcher.hpp @@ -50,7 +50,11 @@ private: struct State { public: - State(std::function func) : m_func(func), m_signal(nullptr) { } + State(std::function func) : + m_func(func), + m_signal(nullptr) + { + } const std::function m_func; std::queue m_invocations; @@ -71,7 +75,7 @@ private: ::_apply_polyfill::apply(tuple, m_state->m_func); m_state->m_invocations.pop(); } - m_state->m_signal = nullptr; + m_state->m_signal.reset(); } }; const std::shared_ptr> m_signal; From ef63a8829c405b2b3bc9f6cefc8fc5335526fa0a Mon Sep 17 00:00:00 2001 From: blagoev Date: Sun, 7 May 2017 02:26:14 +0300 Subject: [PATCH 05/15] Initial download api support --- CHANGELOG.md | 2 +- lib/extensions.js | 28 ++++++++++ src/js_realm.hpp | 38 +++++++++++++ tests/js/session-tests.js | 110 ++++++++++++++++++++++++++++++++++++-- tests/spec/unit_tests.js | 4 ++ 5 files changed, 176 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0bb8ffca..61486ae9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ Old files can still be opened and files open in read-only mode will not be modif * The SyncConfig now gets two more optional parameters, `validate_ssl` and `ssl_trust_certificate_path`. ### Enhancements -* None +* Add Realm open async API support. ### Bug fixes * None diff --git a/lib/extensions.js b/lib/extensions.js index 727b910a..999deeb9 100644 --- a/lib/extensions.js +++ b/lib/extensions.js @@ -40,6 +40,34 @@ module.exports = function(realmConstructor) { setConstructorOnPrototype(realmConstructor.Results); setConstructorOnPrototype(realmConstructor.Object); + //Add async open API + Object.defineProperties(realmConstructor, getOwnPropertyDescriptors({ + open(config) { + return new Promise((resolve, reject) => { + const realm = new Realm(config); + realm.wait((error) => { + if (error) { + reject(new Error("Cannot asynchronously open synced Realm, because the associated session previously experienced a fatal error")); + } + + resolve(realm); + }); + }); + }, + + openAsync(config, callback) { + const realm = new Realm(config); + realm.wait((error) => { + if (error) { + callback(new Error("Cannot asynchronously open synced Realm, because the associated session previously experienced a fatal error")); + return; + } + + callback(null, realm); + }); + } + })); + // Add sync methods if (realmConstructor.Sync) { let userMethods = require('./user-methods'); diff --git a/src/js_realm.hpp b/src/js_realm.hpp index b96e1eec..f76f5876 100644 --- a/src/js_realm.hpp +++ b/src/js_realm.hpp @@ -164,6 +164,8 @@ class RealmClass : public ClassDefinition> { public: using ObjectDefaultsMap = typename Schema::ObjectDefaultsMap; using ConstructorMap = typename Schema::ConstructorMap; + + using WaitHandler = void(); static FunctionType create_constructor(ContextType); @@ -175,6 +177,7 @@ public: static void delete_all(ContextType, FunctionType, ObjectType, size_t, const ValueType[], ReturnValue &); static void write(ContextType, FunctionType, ObjectType, size_t, const ValueType[], ReturnValue &); static void add_listener(ContextType, FunctionType, ObjectType, size_t, const ValueType[], ReturnValue &); + static void wait_for_download_completion(ContextType, FunctionType, ObjectType, size_t, const ValueType[], ReturnValue &); static void remove_listener(ContextType, FunctionType, ObjectType, size_t, const ValueType[], ReturnValue &); static void remove_all_listeners(ContextType, FunctionType, ObjectType, size_t, const ValueType[], ReturnValue &); static void close(ContextType, FunctionType, ObjectType, size_t, const ValueType[], ReturnValue &); @@ -220,6 +223,7 @@ public: {"deleteAll", wrap}, {"write", wrap}, {"addListener", wrap}, + {"wait", wrap}, {"removeListener", wrap}, {"removeAllListeners", wrap}, {"close", wrap}, @@ -537,6 +541,40 @@ void RealmClass::get_sync_session(ContextType ctx, ObjectType object, ReturnV } } + +template +void RealmClass::wait_for_download_completion(ContextType ctx, FunctionType, ObjectType this_object, size_t argc, const ValueType arguments[], ReturnValue &return_value) { + validate_argument_count(argc, 1); + auto callback = Value::validated_to_function(ctx, arguments[0]); + + SharedRealm realm = *get_internal>(this_object); + + Protected protected_callback(ctx, callback); + Protected protected_this(ctx, this_object); + Protected protected_ctx(Context::get_global_context(ctx)); + + EventLoopDispatcher wait_handler([=]() { + HANDLESCOPE + //Function::call(protected_ctx, protected_callback, protected_this, 0, nullptr); + Function::callback(protected_ctx, protected_callback, protected_this, 0, nullptr); + + //::node::MakeCallback(context->GetIsolate(), context->Global(), callback, 1, arguments); + //::v8::Isolate::GetCurrent()->RunMicrotasks(); + }); + std::function waitFunc = std::move(wait_handler); + + if (std::shared_ptr session = SyncManager::shared().get_existing_active_session(realm->config().path)) { + session->wait_for_download_completion([=](std::error_code error_code) { + waitFunc(); + }); + return; + } + + ValueType callback_arguments[1]; + callback_arguments[0] = Value::from_null(ctx); + Function::call(ctx, callback, this_object, 1, callback_arguments); +} + #endif template diff --git a/tests/js/session-tests.js b/tests/js/session-tests.js index c0f88575..46ede801 100644 --- a/tests/js/session-tests.js +++ b/tests/js/session-tests.js @@ -50,17 +50,25 @@ module.exports = { testProperties() { return promisifiedRegister('http://localhost:9080', uuid(), 'password').then(user => { - return new Promise((resolve, _reject) => { + return new Promise((resolve, reject) => { + const accessTokenRefreshed = this; + let successCounter = 0; + function checkSuccess() { + successCounter++; + if (successCounter == 2) { + resolve(); + } + } function postTokenRefreshChecks(sender, error) { try { TestCase.assertEqual(error, accessTokenRefreshed); TestCase.assertEqual(sender.url, `realm://localhost:9080/${user.identity}/myrealm`); - resolve(); + checkSuccess(); } catch (e) { - _reject(e) + reject(e) } }; @@ -70,14 +78,106 @@ module.exports = { const config = { sync: { user, url: 'realm://localhost:9080/~/myrealm', error: postTokenRefreshChecks } }; const realm = new Realm(config); const 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'); + checkSuccess(); + }); + }); + }, + + testPropertiesWithOpen() { + return promisifiedRegister('http://localhost:9080', uuid(), 'password').then(user => { + return new Promise((resolve, reject) => { + + const accessTokenRefreshed = this; + let successCounter = 0; + + function checkSuccess() { + successCounter++; + if (successCounter == 2) { + resolve(); + } + } + + function postTokenRefreshChecks(sender, error) { + try { + TestCase.assertEqual(error, accessTokenRefreshed); + TestCase.assertEqual(sender.url, `realm://localhost:9080/${user.identity}/myrealm`); + checkSuccess(); + } + catch (e) { + reject(e) + } + }; + + // Let the error handler trigger our checks when the access token was refreshed. + postTokenRefreshChecks._notifyOnAccessTokenRefreshed = accessTokenRefreshed; + + const config = { sync: { user, url: 'realm://localhost:9080/~/myrealm', error: postTokenRefreshChecks } }; + Realm.open(config) + .then(realm => { + const 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.assertEqual(session.state, 'active'); + checkSuccess(); + }) + .catch(e => reject(e)); + }); + }); + }, + + + testPropertiesWithOpenAsync() { + return promisifiedRegister('http://localhost:9080', uuid(), 'password').then(user => { + return new Promise((resolve, reject) => { + + const accessTokenRefreshed = this; + let successCounter = 0; + + function checkSuccess() { + successCounter++; + if (successCounter == 2) { + resolve(); + } + } + + function postTokenRefreshChecks(sender, error) { + try { + TestCase.assertEqual(error, accessTokenRefreshed); + TestCase.assertEqual(sender.url, `realm://localhost:9080/${user.identity}/myrealm`); + checkSuccess(); + } + catch (e) { + reject(e) + } + }; + + // Let the error handler trigger our checks when the access token was refreshed. + postTokenRefreshChecks._notifyOnAccessTokenRefreshed = accessTokenRefreshed; + + const config = { sync: { user, url: 'realm://localhost:9080/~/myrealm', error: postTokenRefreshChecks } }; + Realm.openAsync(config, (error, realm) => { + if (error) { + reject(error); + return; + } + + const 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.assertEqual(session.state, 'active'); + + checkSuccess(); + }); }); }); }, diff --git a/tests/spec/unit_tests.js b/tests/spec/unit_tests.js index d84d349c..2826381b 100644 --- a/tests/spec/unit_tests.js +++ b/tests/spec/unit_tests.js @@ -28,6 +28,10 @@ const Realm = require('realm'); const RealmTests = require('../js'); jasmine.DEFAULT_TIMEOUT_INTERVAL = 30000; +const isDebuggerAttached = typeof v8debug === 'object'; +if (isDebuggerAttached) { + jasmine.DEFAULT_TIMEOUT_INTERVAL = 3000000; +} // Create this method with appropriate implementation for Node testing. Realm.copyBundledRealmFiles = function() { From 22987f0a8f92810b5816b519353ef65d8a0f2d18 Mon Sep 17 00:00:00 2001 From: blagoev Date: Mon, 8 May 2017 00:32:13 +0300 Subject: [PATCH 06/15] fix Realm.open to use the correct ctor function --- lib/extensions.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/extensions.js b/lib/extensions.js index 999deeb9..d431c3ab 100644 --- a/lib/extensions.js +++ b/lib/extensions.js @@ -44,7 +44,7 @@ module.exports = function(realmConstructor) { Object.defineProperties(realmConstructor, getOwnPropertyDescriptors({ open(config) { return new Promise((resolve, reject) => { - const realm = new Realm(config); + const realm = new realmConstructor(config); realm.wait((error) => { if (error) { reject(new Error("Cannot asynchronously open synced Realm, because the associated session previously experienced a fatal error")); @@ -56,7 +56,7 @@ module.exports = function(realmConstructor) { }, openAsync(config, callback) { - const realm = new Realm(config); + const realm = new realmConstructor(config); realm.wait((error) => { if (error) { callback(new Error("Cannot asynchronously open synced Realm, because the associated session previously experienced a fatal error")); From 6cd8eefe5d0964aa36f1e0c251fe7adc03534195 Mon Sep 17 00:00:00 2001 From: blagoev Date: Thu, 11 May 2017 23:22:28 +0300 Subject: [PATCH 07/15] Fix download api to wait correctly using schema less realm Added tests using two processes (nodes only) --- lib/extensions.js | 26 +-- src/js_realm.hpp | 70 ++++--- tests/js/download-api-helper.js | 31 +++ tests/js/session-tests.js | 342 +++++++++++++++++--------------- 4 files changed, 275 insertions(+), 194 deletions(-) create mode 100644 tests/js/download-api-helper.js diff --git a/lib/extensions.js b/lib/extensions.js index d431c3ab..c21909fb 100644 --- a/lib/extensions.js +++ b/lib/extensions.js @@ -44,28 +44,28 @@ module.exports = function(realmConstructor) { Object.defineProperties(realmConstructor, getOwnPropertyDescriptors({ open(config) { return new Promise((resolve, reject) => { - const realm = new realmConstructor(config); - realm.wait((error) => { + realmConstructor._waitForDownload(config, (error) => { if (error) { reject(new Error("Cannot asynchronously open synced Realm, because the associated session previously experienced a fatal error")); } - resolve(realm); + let syncedRealm = new realmConstructor(config); + resolve(syncedRealm); }); }); }, - openAsync(config, callback) { - const realm = new realmConstructor(config); - realm.wait((error) => { - if (error) { - callback(new Error("Cannot asynchronously open synced Realm, because the associated session previously experienced a fatal error")); - return; - } + // openAsync(config, callback) { + // const realm = new realmConstructor(config); + // realm.wait((error) => { + // if (error) { + // callback(new Error("Cannot asynchronously open synced Realm, because the associated session previously experienced a fatal error")); + // return; + // } - callback(null, realm); - }); - } + // callback(null, realm); + // }); + // } })); // Add sync methods diff --git a/src/js_realm.hpp b/src/js_realm.hpp index f76f5876..a807ff6f 100644 --- a/src/js_realm.hpp +++ b/src/js_realm.hpp @@ -209,6 +209,7 @@ public: {"schemaVersion", wrap}, {"clearTestState", wrap}, {"copyBundledRealmFiles", wrap}, + {"_waitForDownload", wrap}, }; PropertyMap const static_properties = { @@ -223,7 +224,6 @@ public: {"deleteAll", wrap}, {"write", wrap}, {"addListener", wrap}, - {"wait", wrap}, {"removeListener", wrap}, {"removeAllListeners", wrap}, {"close", wrap}, @@ -544,35 +544,55 @@ void RealmClass::get_sync_session(ContextType ctx, ObjectType object, ReturnV template void RealmClass::wait_for_download_completion(ContextType ctx, FunctionType, ObjectType this_object, size_t argc, const ValueType arguments[], ReturnValue &return_value) { - validate_argument_count(argc, 1); - auto callback = Value::validated_to_function(ctx, arguments[0]); + validate_argument_count(argc, 2); + auto config_object = Value::validated_to_object(ctx, arguments[0]); + auto callback_function = Value::validated_to_function(ctx, arguments[1]); - SharedRealm realm = *get_internal>(this_object); + ValueType sync_config_value = Object::get_property(ctx, config_object, "sync"); + if (!Value::is_undefined(ctx, sync_config_value)) + { + realm::Realm::Config config; + static const String encryption_key_string = "encryptionKey"; + ValueType encryption_key_value = Object::get_property(ctx, config_object, encryption_key_string); + if (!Value::is_undefined(ctx, encryption_key_value)) + { + std::string encryption_key = NativeAccessor::to_binary(ctx, encryption_key_value); + config.encryption_key = std::vector(encryption_key.begin(), encryption_key.end()); + } + + Protected thiz(ctx, this_object); + SyncClass::populate_sync_config(ctx, thiz, config_object, config); - Protected protected_callback(ctx, callback); - Protected protected_this(ctx, this_object); - Protected protected_ctx(Context::get_global_context(ctx)); + Protected protected_callback(ctx, callback_function); + Protected protected_this(ctx, this_object); + Protected protected_ctx(Context::get_global_context(ctx)); - EventLoopDispatcher wait_handler([=]() { - HANDLESCOPE - //Function::call(protected_ctx, protected_callback, protected_this, 0, nullptr); - Function::callback(protected_ctx, protected_callback, protected_this, 0, nullptr); - - //::node::MakeCallback(context->GetIsolate(), context->Global(), callback, 1, arguments); - //::v8::Isolate::GetCurrent()->RunMicrotasks(); - }); - std::function waitFunc = std::move(wait_handler); - - if (std::shared_ptr session = SyncManager::shared().get_existing_active_session(realm->config().path)) { - session->wait_for_download_completion([=](std::error_code error_code) { - waitFunc(); + EventLoopDispatcher wait_handler([=]() { + HANDLESCOPE + Function::callback(protected_ctx, protected_callback, protected_this, 0, nullptr); }); - return; + std::function waitFunc = std::move(wait_handler); + + auto realm = realm::Realm::get_shared_realm(config); + if (auto sync_config = config.sync_config) + { + std::shared_ptr user = sync_config->user; + if (user && user->state() != SyncUser::State::Error) + { + auto session = user->session_for_on_disk_path(config.path); + session->wait_for_download_completion([=](std::error_code error_code) { + realm->config(); + waitFunc(); + }); + return; + } + } + } - - ValueType callback_arguments[1]; - callback_arguments[0] = Value::from_null(ctx); - Function::call(ctx, callback, this_object, 1, callback_arguments); + + ValueType callback_arguments[1]; + callback_arguments[0] = Value::from_null(ctx); + Function::call(ctx, callback_function, this_object, 1, callback_arguments); } #endif diff --git a/tests/js/download-api-helper.js b/tests/js/download-api-helper.js new file mode 100644 index 00000000..b73d000e --- /dev/null +++ b/tests/js/download-api-helper.js @@ -0,0 +1,31 @@ +'use strict'; + +const username = process.argv[2]; +const realmName = process.argv[3]; +const realmModule = process.argv[4]; + +var Realm = require(realmModule); +Realm.Sync.User.register('http://localhost:9080', username, 'password', (error, user) => { + if (error) { + console.log(error); + process.exit(-2); + } else { + const config = { + sync: { user, url: `realm://localhost:9080/~/${realmName}`, error: err => console.log(err) }, + schema: [{ name: 'Dog', properties: { name: 'string' } }] + }; + + var realm = new Realm(config); + + realm.write(() => { + for (let i = 1; i <= 3; i++) { + realm.create('Dog', { name: `Lassy ${i}` }); + } + }); + + console.log("Dogs count " + realm.objects('Dog').length); + setTimeout(_ => process.exit(0), 3000); + } +}); + + diff --git a/tests/js/session-tests.js b/tests/js/session-tests.js index 46ede801..684d10c3 100644 --- a/tests/js/session-tests.js +++ b/tests/js/session-tests.js @@ -22,18 +22,37 @@ const Realm = require('realm'); const TestCase = require('./asserts'); +const tmp = require('tmp'); +const fs = require('fs'); +const execFile = require('child_process').execFile; + +tmp.setGracefulCleanup(); 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); - }); + 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) { + console.log(`promisifiedRegister ${error}`); + reject(error); + } else { + resolve(user); + } + }); + }); +} + +function promisifiedLogin(server, username, password) { + return new Promise((resolve, reject) => { + Realm.Sync.User.login(server, username, password, (error, user) => { + if (error) { + console.log(`promisifiedLogin ${error}`); reject(error); } else { resolve(user); @@ -43,165 +62,176 @@ function promisifiedRegister(server, username, password) { } module.exports = { - testLocalRealmHasNoSession() { - let realm = new Realm(); - TestCase.assertNull(realm.syncSession); - }, - testProperties() { - return promisifiedRegister('http://localhost:9080', uuid(), 'password').then(user => { - return new Promise((resolve, reject) => { - - const accessTokenRefreshed = this; - let successCounter = 0; - function checkSuccess() { - successCounter++; - if (successCounter == 2) { - resolve(); - } + // testLocalRealmHasNoSession() { + // let realm = new Realm(); + // TestCase.assertNull(realm.syncSession); + // }, + + // testProperties() { + // return promisifiedRegister('http://localhost:9080', uuid(), 'password').then(user => { + // return new Promise((resolve, reject) => { + + // const accessTokenRefreshed = this; + // let successCounter = 0; + // function checkSuccess() { + // successCounter++; + // if (successCounter == 2) { + // resolve(); + // } + // } + + // function postTokenRefreshChecks(sender, error) { + // try { + // TestCase.assertEqual(error, accessTokenRefreshed); + // TestCase.assertEqual(sender.url, `realm://localhost:9080/${user.identity}/myrealm`); + // checkSuccess(); + // } + // catch (e) { + // reject(e) + // } + // }; + + // // Let the error handler trigger our checks when the access token was refreshed. + // postTokenRefreshChecks._notifyOnAccessTokenRefreshed = accessTokenRefreshed; + + // const config = { sync: { user, url: 'realm://localhost:9080/~/myrealm', error: postTokenRefreshChecks } }; + // const realm = new Realm(config); + // const 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'); + // checkSuccess(); + // }); + // }); + // }, + + testRealmOpen() { + const isNodeProccess = typeof process === 'object'; + + if (!isNodeProccess) { + return Promise.resolve(); + } + + const username = uuid(); + const username2 = uuid(); + const realmName = uuid(); + const expectedObjectsCount = 3; + + let tmpDir = tmp.dirSync(); + let content = fs.readFileSync(__dirname + '/download-api-helper.js', 'utf8'); + let tmpFile = tmp.fileSync({ dir: tmpDir.name }); + fs.appendFileSync(tmpFile.fd, content, { encoding : 'utf8' }); + + return new Promise((resolve, reject) => { + const child = execFile('node', [tmpFile.name, username, realmName, REALM_MODULE_PATH], { cwd: tmpDir.name }, (error, stdout, stderr) => { + if (error) { + reject(new Error('Error executing download api helper' + error)); } - - function postTokenRefreshChecks(sender, error) { - try { - TestCase.assertEqual(error, accessTokenRefreshed); - TestCase.assertEqual(sender.url, `realm://localhost:9080/${user.identity}/myrealm`); - checkSuccess(); - } - catch (e) { - reject(e) - } - }; - - // Let the error handler trigger our checks when the access token was refreshed. - postTokenRefreshChecks._notifyOnAccessTokenRefreshed = accessTokenRefreshed; - - const config = { sync: { user, url: 'realm://localhost:9080/~/myrealm', error: postTokenRefreshChecks } }; - const realm = new Realm(config); - const 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'); - checkSuccess(); + resolve(); }); - }); - }, + }) + .then(() => { + return promisifiedLogin('http://localhost:9080', username, 'password').then(user => { + return new Promise((resolve, reject) => { + const accessTokenRefreshed = this; + let successCounter = 0; - testPropertiesWithOpen() { - return promisifiedRegister('http://localhost:9080', uuid(), 'password').then(user => { - return new Promise((resolve, reject) => { + let config = { + sync: { user, url: `realm://localhost:9080/~/${realmName}` }, + schema: [{ name: 'Dog', properties: { name: 'string' } }], + }; - const accessTokenRefreshed = this; - let successCounter = 0; + Realm.open(config) + .then(realm => { + let actualObjectsCount = realm.objects('Dog').length; + TestCase.assertEqual(actualObjectsCount, expectedObjectsCount, "Synced realm does not contain the expected objects count"); - function checkSuccess() { - successCounter++; - if (successCounter == 2) { - resolve(); - } - } - - function postTokenRefreshChecks(sender, error) { - try { - TestCase.assertEqual(error, accessTokenRefreshed); - TestCase.assertEqual(sender.url, `realm://localhost:9080/${user.identity}/myrealm`); - checkSuccess(); - } - catch (e) { - reject(e) - } - }; - - // Let the error handler trigger our checks when the access token was refreshed. - postTokenRefreshChecks._notifyOnAccessTokenRefreshed = accessTokenRefreshed; - - const config = { sync: { user, url: 'realm://localhost:9080/~/myrealm', error: postTokenRefreshChecks } }; - Realm.open(config) - .then(realm => { - const 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.assertEqual(session.state, 'active'); - checkSuccess(); - }) - .catch(e => reject(e)); - }); - }); - }, - - - testPropertiesWithOpenAsync() { - return promisifiedRegister('http://localhost:9080', uuid(), 'password').then(user => { - return new Promise((resolve, reject) => { - - const accessTokenRefreshed = this; - let successCounter = 0; - - function checkSuccess() { - successCounter++; - if (successCounter == 2) { - resolve(); - } - } - - function postTokenRefreshChecks(sender, error) { - try { - TestCase.assertEqual(error, accessTokenRefreshed); - TestCase.assertEqual(sender.url, `realm://localhost:9080/${user.identity}/myrealm`); - checkSuccess(); - } - catch (e) { - reject(e) - } - }; - - // Let the error handler trigger our checks when the access token was refreshed. - postTokenRefreshChecks._notifyOnAccessTokenRefreshed = accessTokenRefreshed; - - const config = { sync: { user, url: 'realm://localhost:9080/~/myrealm', error: postTokenRefreshChecks } }; - Realm.openAsync(config, (error, realm) => { - if (error) { - reject(error); - return; - } - - const 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.assertEqual(session.state, 'active'); - - checkSuccess(); + const 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.assertEqual(session.state, 'active'); + resolve(); + }).catch(e => { reject(e)}); }); }); }); - }, - - testErrorHandling() { - return promisifiedRegister('http://localhost:9080', uuid(), 'password').then(user => { - return new Promise((resolve, _reject) => { - const config = { sync: { user, url: 'realm://localhost:9080/~/myrealm' } }; - config.sync.error = (sender, error) => { - try { - TestCase.assertEqual(error.message, 'simulated error'); - TestCase.assertEqual(error.code, 123); - resolve(); - } - catch (e) { - _reject(e); - } - }; - const realm = new Realm(config); - const session = realm.syncSession; - - TestCase.assertEqual(session.config.error, config.sync.error); - session._simulateError(123, 'simulated error'); - }); - }); } -} + + + // testPropertiesWithOpenAsync() { + // return promisifiedRegister('http://localhost:9080', uuid(), 'password').then(user => { + // return new Promise((resolve, reject) => { + + // const accessTokenRefreshed = this; + // let successCounter = 0; + + // function checkSuccess() { + // successCounter++; + // if (successCounter == 2) { + // resolve(); + // } + // } + + // function postTokenRefreshChecks(sender, error) { + // try { + // TestCase.assertEqual(error, accessTokenRefreshed); + // TestCase.assertEqual(sender.url, `realm://localhost:9080/${user.identity}/myrealm`); + // checkSuccess(); + // } + // catch (e) { + // reject(e) + // } + // }; + + // // Let the error handler trigger our checks when the access token was refreshed. + // postTokenRefreshChecks._notifyOnAccessTokenRefreshed = accessTokenRefreshed; + + // const config = { sync: { user, url: 'realm://localhost:9080/~/myrealm', error: postTokenRefreshChecks } }; + // Realm.openAsync(config, (error, realm) => { + // if (error) { + // reject(error); + // return; + // } + + // const 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.assertEqual(session.state, 'active'); + + // checkSuccess(); + // }); + // }); + // }); + // }, + + // testErrorHandling() { + // return promisifiedRegister('http://localhost:9080', uuid(), 'password').then(user => { + // return new Promise((resolve, _reject) => { + // const config = { sync: { user, url: 'realm://localhost:9080/~/myrealm' } }; + // config.sync.error = (sender, error) => { + // try { + // TestCase.assertEqual(error.message, 'simulated error'); + // TestCase.assertEqual(error.code, 123); + // resolve(); + // } + // catch (e) { + // _reject(e); + // } + // }; + // const realm = new Realm(config); + // const session = realm.syncSession; + + // TestCase.assertEqual(session.config.error, config.sync.error); + // session._simulateError(123, 'simulated error'); + // }); + // }); + // } +} \ No newline at end of file From 40737302590d2aac5795836381d4219a7b8f6e2c Mon Sep 17 00:00:00 2001 From: blagoev Date: Thu, 11 May 2017 23:31:50 +0300 Subject: [PATCH 08/15] Fix showing syntax errors in sync test files --- tests/js/index.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/js/index.js b/tests/js/index.js index 5ad9b114..5b6580f8 100644 --- a/tests/js/index.js +++ b/tests/js/index.js @@ -35,11 +35,16 @@ if (!(typeof process === 'object' && process.platform === 'win32')) { } // If sync is enabled, run the user tests +let hasSync = false; try { Realm.Sync; // This will throw if Sync is disabled. + hasSync = true; +} catch (e) { } + +if (hasSync) { TESTS.UserTests = require('./user-tests'); TESTS.SessionTests = require('./session-tests'); -} catch (e) {} +} function node_require(module) { return require(module); } From 6c3dc8258a252bafc61da05e3bbbc392b1007290 Mon Sep 17 00:00:00 2001 From: blagoev Date: Thu, 11 May 2017 23:54:29 +0300 Subject: [PATCH 09/15] enable all session-tests and Realm.openAsync --- lib/extensions.js | 20 +-- tests/js/session-tests.js | 283 ++++++++++++++++++++------------------ 2 files changed, 158 insertions(+), 145 deletions(-) diff --git a/lib/extensions.js b/lib/extensions.js index c21909fb..1bd0a0c7 100644 --- a/lib/extensions.js +++ b/lib/extensions.js @@ -55,17 +55,17 @@ module.exports = function(realmConstructor) { }); }, - // openAsync(config, callback) { - // const realm = new realmConstructor(config); - // realm.wait((error) => { - // if (error) { - // callback(new Error("Cannot asynchronously open synced Realm, because the associated session previously experienced a fatal error")); - // return; - // } + openAsync(config, callback) { + realmConstructor._waitForDownload(config, (error) => { + if (error) { + callback(new Error("Cannot asynchronously open synced Realm, because the associated session previously experienced a fatal error")); + } - // callback(null, realm); - // }); - // } + let syncedRealm = new realmConstructor(config); + callback(null, syncedRealm); + }); + + }, })); // Add sync methods diff --git a/tests/js/session-tests.js b/tests/js/session-tests.js index 684d10c3..a72a5316 100644 --- a/tests/js/session-tests.js +++ b/tests/js/session-tests.js @@ -63,51 +63,51 @@ function promisifiedLogin(server, username, password) { module.exports = { - // testLocalRealmHasNoSession() { - // let realm = new Realm(); - // TestCase.assertNull(realm.syncSession); - // }, + testLocalRealmHasNoSession() { + let realm = new Realm(); + TestCase.assertNull(realm.syncSession); + }, - // testProperties() { - // return promisifiedRegister('http://localhost:9080', uuid(), 'password').then(user => { - // return new Promise((resolve, reject) => { + testProperties() { + return promisifiedRegister('http://localhost:9080', uuid(), 'password').then(user => { + return new Promise((resolve, reject) => { - // const accessTokenRefreshed = this; - // let successCounter = 0; - // function checkSuccess() { - // successCounter++; - // if (successCounter == 2) { - // resolve(); - // } - // } + const accessTokenRefreshed = this; + let successCounter = 0; + function checkSuccess() { + successCounter++; + if (successCounter == 2) { + resolve(); + } + } - // function postTokenRefreshChecks(sender, error) { - // try { - // TestCase.assertEqual(error, accessTokenRefreshed); - // TestCase.assertEqual(sender.url, `realm://localhost:9080/${user.identity}/myrealm`); - // checkSuccess(); - // } - // catch (e) { - // reject(e) - // } - // }; + function postTokenRefreshChecks(sender, error) { + try { + TestCase.assertEqual(error, accessTokenRefreshed); + TestCase.assertEqual(sender.url, `realm://localhost:9080/${user.identity}/myrealm`); + checkSuccess(); + } + catch (e) { + reject(e) + } + }; - // // Let the error handler trigger our checks when the access token was refreshed. - // postTokenRefreshChecks._notifyOnAccessTokenRefreshed = accessTokenRefreshed; + // Let the error handler trigger our checks when the access token was refreshed. + postTokenRefreshChecks._notifyOnAccessTokenRefreshed = accessTokenRefreshed; - // const config = { sync: { user, url: 'realm://localhost:9080/~/myrealm', error: postTokenRefreshChecks } }; - // const realm = new Realm(config); - // const 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'); - // checkSuccess(); - // }); - // }); - // }, + const config = { sync: { user, url: 'realm://localhost:9080/~/myrealm', error: postTokenRefreshChecks } }; + const realm = new Realm(config); + const 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'); + checkSuccess(); + }); + }); + }, testRealmOpen() { const isNodeProccess = typeof process === 'object'; @@ -115,7 +115,7 @@ module.exports = { if (!isNodeProccess) { return Promise.resolve(); } - + const username = uuid(); const username2 = uuid(); const realmName = uuid(); @@ -124,7 +124,7 @@ module.exports = { let tmpDir = tmp.dirSync(); let content = fs.readFileSync(__dirname + '/download-api-helper.js', 'utf8'); let tmpFile = tmp.fileSync({ dir: tmpDir.name }); - fs.appendFileSync(tmpFile.fd, content, { encoding : 'utf8' }); + fs.appendFileSync(tmpFile.fd, content, { encoding: 'utf8' }); return new Promise((resolve, reject) => { const child = execFile('node', [tmpFile.name, username, realmName, REALM_MODULE_PATH], { cwd: tmpDir.name }, (error, stdout, stderr) => { @@ -134,104 +134,117 @@ module.exports = { resolve(); }); }) - .then(() => { - return promisifiedLogin('http://localhost:9080', username, 'password').then(user => { - return new Promise((resolve, reject) => { - const accessTokenRefreshed = this; - let successCounter = 0; + .then(() => { + return promisifiedLogin('http://localhost:9080', username, 'password').then(user => { + return new Promise((resolve, reject) => { + const accessTokenRefreshed = this; + let successCounter = 0; - let config = { - sync: { user, url: `realm://localhost:9080/~/${realmName}` }, - schema: [{ name: 'Dog', properties: { name: 'string' } }], - }; + let config = { + sync: { user, url: `realm://localhost:9080/~/${realmName}` }, + schema: [{ name: 'Dog', properties: { name: 'string' } }], + }; - Realm.open(config) - .then(realm => { - let actualObjectsCount = realm.objects('Dog').length; - TestCase.assertEqual(actualObjectsCount, expectedObjectsCount, "Synced realm does not contain the expected objects count"); + Realm.open(config) + .then(realm => { + let actualObjectsCount = realm.objects('Dog').length; + TestCase.assertEqual(actualObjectsCount, expectedObjectsCount, "Synced realm does not contain the expected objects count"); - const 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.assertEqual(session.state, 'active'); - resolve(); - }).catch(e => { reject(e)}); + const 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.assertEqual(session.state, 'active'); + resolve(); + }).catch(e => { reject(e) }); + }); }); }); + }, + + testRealmOpenAsync() { + const isNodeProccess = typeof process === 'object'; + + if (!isNodeProccess) { + return Promise.resolve(); + } + + const username = uuid(); + const username2 = uuid(); + const realmName = uuid(); + const expectedObjectsCount = 3; + + let tmpDir = tmp.dirSync(); + let content = fs.readFileSync(__dirname + '/download-api-helper.js', 'utf8'); + let tmpFile = tmp.fileSync({ dir: tmpDir.name }); + fs.appendFileSync(tmpFile.fd, content, { encoding: 'utf8' }); + + return new Promise((resolve, reject) => { + const child = execFile('node', [tmpFile.name, username, realmName, REALM_MODULE_PATH], { cwd: tmpDir.name }, (error, stdout, stderr) => { + if (error) { + reject(new Error('Error executing download api helper' + error)); + } + resolve(); + }); + }) + .then(() => { + return promisifiedLogin('http://localhost:9080', username, 'password').then(user => { + return new Promise((resolve, reject) => { + const accessTokenRefreshed = this; + let successCounter = 0; + + let config = { + sync: { user, url: `realm://localhost:9080/~/${realmName}` }, + schema: [{ name: 'Dog', properties: { name: 'string' } }], + }; + + Realm.openAsync(config, (error, realm) => { + try { + if (error) { + reject(error); + } + + let actualObjectsCount = realm.objects('Dog').length; + TestCase.assertEqual(actualObjectsCount, expectedObjectsCount, "Synced realm does not contain the expected objects count"); + + const 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.assertEqual(session.state, 'active'); + resolve(); + } + catch (e) { + reject(e); + } + }); + }); + }); + }); + }, + + testErrorHandling() { + return promisifiedRegister('http://localhost:9080', uuid(), 'password').then(user => { + return new Promise((resolve, _reject) => { + const config = { sync: { user, url: 'realm://localhost:9080/~/myrealm' } }; + config.sync.error = (sender, error) => { + try { + TestCase.assertEqual(error.message, 'simulated error'); + TestCase.assertEqual(error.code, 123); + resolve(); + } + catch (e) { + _reject(e); + } + }; + const realm = new Realm(config); + const session = realm.syncSession; + + TestCase.assertEqual(session.config.error, config.sync.error); + session._simulateError(123, 'simulated error'); + }); }); } - - - // testPropertiesWithOpenAsync() { - // return promisifiedRegister('http://localhost:9080', uuid(), 'password').then(user => { - // return new Promise((resolve, reject) => { - - // const accessTokenRefreshed = this; - // let successCounter = 0; - - // function checkSuccess() { - // successCounter++; - // if (successCounter == 2) { - // resolve(); - // } - // } - - // function postTokenRefreshChecks(sender, error) { - // try { - // TestCase.assertEqual(error, accessTokenRefreshed); - // TestCase.assertEqual(sender.url, `realm://localhost:9080/${user.identity}/myrealm`); - // checkSuccess(); - // } - // catch (e) { - // reject(e) - // } - // }; - - // // Let the error handler trigger our checks when the access token was refreshed. - // postTokenRefreshChecks._notifyOnAccessTokenRefreshed = accessTokenRefreshed; - - // const config = { sync: { user, url: 'realm://localhost:9080/~/myrealm', error: postTokenRefreshChecks } }; - // Realm.openAsync(config, (error, realm) => { - // if (error) { - // reject(error); - // return; - // } - - // const 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.assertEqual(session.state, 'active'); - - // checkSuccess(); - // }); - // }); - // }); - // }, - - // testErrorHandling() { - // return promisifiedRegister('http://localhost:9080', uuid(), 'password').then(user => { - // return new Promise((resolve, _reject) => { - // const config = { sync: { user, url: 'realm://localhost:9080/~/myrealm' } }; - // config.sync.error = (sender, error) => { - // try { - // TestCase.assertEqual(error.message, 'simulated error'); - // TestCase.assertEqual(error.code, 123); - // resolve(); - // } - // catch (e) { - // _reject(e); - // } - // }; - // const realm = new Realm(config); - // const session = realm.syncSession; - - // TestCase.assertEqual(session.config.error, config.sync.error); - // session._simulateError(123, 'simulated error'); - // }); - // }); - // } } \ No newline at end of file From 6be9ad5e3520c555de859cd6b20fe278235073c3 Mon Sep 17 00:00:00 2001 From: blagoev Date: Fri, 12 May 2017 00:04:19 +0300 Subject: [PATCH 10/15] Add a fix for RN hang with promises --- lib/extensions.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/extensions.js b/lib/extensions.js index 1bd0a0c7..2ab8a789 100644 --- a/lib/extensions.js +++ b/lib/extensions.js @@ -50,7 +50,7 @@ module.exports = function(realmConstructor) { } let syncedRealm = new realmConstructor(config); - resolve(syncedRealm); + setTimeout(_ => { resolve(syncedRealm); }, 1); //FIXME: RN hangs here. Remove when node's makeCallback alternative is implemented }); }); }, @@ -64,7 +64,6 @@ module.exports = function(realmConstructor) { let syncedRealm = new realmConstructor(config); callback(null, syncedRealm); }); - }, })); From 1ae58780bf4b1e395dd0aa2790d9dfd557ad287c Mon Sep 17 00:00:00 2001 From: blagoev Date: Fri, 12 May 2017 01:40:15 +0300 Subject: [PATCH 11/15] Pass the error code and message to user code --- src/js_realm.hpp | 42 +++++++++++++++++++++++++++++++++--------- 1 file changed, 33 insertions(+), 9 deletions(-) diff --git a/src/js_realm.hpp b/src/js_realm.hpp index a807ff6f..10930dbd 100644 --- a/src/js_realm.hpp +++ b/src/js_realm.hpp @@ -165,7 +165,7 @@ public: using ObjectDefaultsMap = typename Schema::ObjectDefaultsMap; using ConstructorMap = typename Schema::ConstructorMap; - using WaitHandler = void(); + using WaitHandler = void(std::error_code); static FunctionType create_constructor(ContextType); @@ -567,9 +567,22 @@ void RealmClass::wait_for_download_completion(ContextType ctx, FunctionType, Protected protected_this(ctx, this_object); Protected protected_ctx(Context::get_global_context(ctx)); - EventLoopDispatcher wait_handler([=]() { + EventLoopDispatcher wait_handler([=](std::error_code error_code) { HANDLESCOPE - Function::callback(protected_ctx, protected_callback, protected_this, 0, nullptr); + if (error_code == std::error_code{}) { + //success + Function::callback(protected_ctx, protected_callback, protected_this, 0, nullptr); + } + else { + //fail + ObjectType object = Object::create_empty(protected_ctx); + Object::set_property(protected_ctx, object, "message", Value::from_string(protected_ctx, error_code.message())); + Object::set_property(protected_ctx, object, "errorCode", Value::from_number(protected_ctx, error_code.value())); + + ValueType callback_arguments[1]; + callback_arguments[0] = object; + Function::callback(protected_ctx, protected_callback, protected_this, 1, callback_arguments); + } }); std::function waitFunc = std::move(wait_handler); @@ -579,13 +592,24 @@ void RealmClass::wait_for_download_completion(ContextType ctx, FunctionType, std::shared_ptr user = sync_config->user; if (user && user->state() != SyncUser::State::Error) { - auto session = user->session_for_on_disk_path(config.path); - session->wait_for_download_completion([=](std::error_code error_code) { - realm->config(); - waitFunc(); - }); - return; + if (auto session = user->session_for_on_disk_path(config.path)) + { + session->wait_for_download_completion([=](std::error_code error_code) { + realm->config(); //capture and keep realm instance for till here + waitFunc(error_code); + }); + return; + } } + + ObjectType object = Object::create_empty(protected_ctx); + Object::set_property(protected_ctx, object, "message", Value::from_string(protected_ctx, "Cannot asynchronously open synced Realm, because the associated session previously experienced a fatal error")); + Object::set_property(protected_ctx, object, "errorCode", Value::from_number(protected_ctx, 1)); + + ValueType callback_arguments[1]; + callback_arguments[0] = object; + Function::call(protected_ctx, protected_callback, protected_this, 1, callback_arguments); + return; } } From d214b72fc69d0460def67215a8313204b847af75 Mon Sep 17 00:00:00 2001 From: blagoev Date: Fri, 12 May 2017 01:41:04 +0300 Subject: [PATCH 12/15] forgotten file --- lib/extensions.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/extensions.js b/lib/extensions.js index 2ab8a789..256149e7 100644 --- a/lib/extensions.js +++ b/lib/extensions.js @@ -46,7 +46,7 @@ module.exports = function(realmConstructor) { return new Promise((resolve, reject) => { realmConstructor._waitForDownload(config, (error) => { if (error) { - reject(new Error("Cannot asynchronously open synced Realm, because the associated session previously experienced a fatal error")); + reject(error); } let syncedRealm = new realmConstructor(config); @@ -58,7 +58,7 @@ module.exports = function(realmConstructor) { openAsync(config, callback) { realmConstructor._waitForDownload(config, (error) => { if (error) { - callback(new Error("Cannot asynchronously open synced Realm, because the associated session previously experienced a fatal error")); + callback(error); } let syncedRealm = new realmConstructor(config); From 45e18a4300e2c522267319dac17cfbe7502e9612 Mon Sep 17 00:00:00 2001 From: blagoev Date: Fri, 12 May 2017 15:38:44 +0300 Subject: [PATCH 13/15] Fix download api for RN fix tests when run in RN --- lib/extensions.js | 6 ++++-- tests/js/session-tests.js | 25 +++++++++++++++++-------- 2 files changed, 21 insertions(+), 10 deletions(-) diff --git a/lib/extensions.js b/lib/extensions.js index 256149e7..3bcdb051 100644 --- a/lib/extensions.js +++ b/lib/extensions.js @@ -50,7 +50,8 @@ module.exports = function(realmConstructor) { } let syncedRealm = new realmConstructor(config); - setTimeout(_ => { resolve(syncedRealm); }, 1); //FIXME: RN hangs here. Remove when node's makeCallback alternative is implemented + //FIXME: RN hangs here. Remove when node's makeCallback alternative is implemented + setTimeout(() => { resolve(syncedRealm); }, 1); }); }); }, @@ -62,7 +63,8 @@ module.exports = function(realmConstructor) { } let syncedRealm = new realmConstructor(config); - callback(null, syncedRealm); + //FIXME: RN hangs here. Remove when node's makeCallback alternative is implemented + setTimeout(() => { callback(null, syncedRealm); }, 1); }); }, })); diff --git a/tests/js/session-tests.js b/tests/js/session-tests.js index a72a5316..51ef57e6 100644 --- a/tests/js/session-tests.js +++ b/tests/js/session-tests.js @@ -22,11 +22,24 @@ const Realm = require('realm'); const TestCase = require('./asserts'); -const tmp = require('tmp'); -const fs = require('fs'); -const execFile = require('child_process').execFile; -tmp.setGracefulCleanup(); +const isNodeProccess = (typeof process === 'object' && process + '' === '[object process]'); +console.log("isnode " + isNodeProccess + " typeof " + typeof process === 'object'); +function node_require(module) { + return require(module); +} + +let tmp; +let fs; +let execFile; + +if (isNodeProccess) { + tmp = node_require('tmp'); + fs = node_require('fs'); + execFile = node_require('child_process').execFile; + tmp.setGracefulCleanup(); +} + function uuid() { return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { @@ -110,8 +123,6 @@ module.exports = { }, testRealmOpen() { - const isNodeProccess = typeof process === 'object'; - if (!isNodeProccess) { return Promise.resolve(); } @@ -164,8 +175,6 @@ module.exports = { }, testRealmOpenAsync() { - const isNodeProccess = typeof process === 'object'; - if (!isNodeProccess) { return Promise.resolve(); } From ab626253e554800510b606fc3b0fdf4f58d01909 Mon Sep 17 00:00:00 2001 From: blagoev Date: Mon, 15 May 2017 15:33:53 +0300 Subject: [PATCH 14/15] addressed pr review comments --- src/js_realm.hpp | 18 +++++-------- tests/js/download-api-helper.js | 3 +++ tests/js/session-tests.js | 48 ++++++++++++++++----------------- 3 files changed, 33 insertions(+), 36 deletions(-) diff --git a/src/js_realm.hpp b/src/js_realm.hpp index 10930dbd..e7a15242 100644 --- a/src/js_realm.hpp +++ b/src/js_realm.hpp @@ -549,13 +549,11 @@ void RealmClass::wait_for_download_completion(ContextType ctx, FunctionType, auto callback_function = Value::validated_to_function(ctx, arguments[1]); ValueType sync_config_value = Object::get_property(ctx, config_object, "sync"); - if (!Value::is_undefined(ctx, sync_config_value)) - { + if (!Value::is_undefined(ctx, sync_config_value)) { realm::Realm::Config config; static const String encryption_key_string = "encryptionKey"; ValueType encryption_key_value = Object::get_property(ctx, config_object, encryption_key_string); - if (!Value::is_undefined(ctx, encryption_key_value)) - { + if (!Value::is_undefined(ctx, encryption_key_value)) { std::string encryption_key = NativeAccessor::to_binary(ctx, encryption_key_value); config.encryption_key = std::vector(encryption_key.begin(), encryption_key.end()); } @@ -587,13 +585,10 @@ void RealmClass::wait_for_download_completion(ContextType ctx, FunctionType, std::function waitFunc = std::move(wait_handler); auto realm = realm::Realm::get_shared_realm(config); - if (auto sync_config = config.sync_config) - { + if (auto sync_config = config.sync_config) { std::shared_ptr user = sync_config->user; - if (user && user->state() != SyncUser::State::Error) - { - if (auto session = user->session_for_on_disk_path(config.path)) - { + if (user && user->state() != SyncUser::State::Error) { + if (auto session = user->session_for_on_disk_path(config.path)) { session->wait_for_download_completion([=](std::error_code error_code) { realm->config(); //capture and keep realm instance for till here waitFunc(error_code); @@ -611,7 +606,6 @@ void RealmClass::wait_for_download_completion(ContextType ctx, FunctionType, Function::call(protected_ctx, protected_callback, protected_this, 1, callback_arguments); return; } - } ValueType callback_arguments[1]; @@ -804,4 +798,4 @@ void RealmClass::close(ContextType ctx, FunctionType, ObjectType this_object, } } // js -} // realm +} // realm \ No newline at end of file diff --git a/tests/js/download-api-helper.js b/tests/js/download-api-helper.js index b73d000e..674e468b 100644 --- a/tests/js/download-api-helper.js +++ b/tests/js/download-api-helper.js @@ -1,3 +1,6 @@ +/* +This script creates 3 new objects into a new realm. These are objects are validated to exists by the download api tests. +*/ 'use strict'; const username = process.argv[2]; diff --git a/tests/js/session-tests.js b/tests/js/session-tests.js index 51ef57e6..08a5fbd7 100644 --- a/tests/js/session-tests.js +++ b/tests/js/session-tests.js @@ -128,7 +128,6 @@ module.exports = { } const username = uuid(); - const username2 = uuid(); const realmName = uuid(); const expectedObjectsCount = 3; @@ -138,6 +137,7 @@ module.exports = { fs.appendFileSync(tmpFile.fd, content, { encoding: 'utf8' }); return new Promise((resolve, reject) => { + //execute download-api-helper which inserts predefined number of objects into the synced realm. const child = execFile('node', [tmpFile.name, username, realmName, REALM_MODULE_PATH], { cwd: tmpDir.name }, (error, stdout, stderr) => { if (error) { reject(new Error('Error executing download api helper' + error)); @@ -145,33 +145,33 @@ module.exports = { resolve(); }); }) - .then(() => { - return promisifiedLogin('http://localhost:9080', username, 'password').then(user => { - return new Promise((resolve, reject) => { - const accessTokenRefreshed = this; - let successCounter = 0; + .then(() => { + return promisifiedLogin('http://localhost:9080', username, 'password').then(user => { + return new Promise((resolve, reject) => { + const accessTokenRefreshed = this; + let successCounter = 0; - let config = { - sync: { user, url: `realm://localhost:9080/~/${realmName}` }, - schema: [{ name: 'Dog', properties: { name: 'string' } }], - }; + let config = { + sync: { user, url: `realm://localhost:9080/~/${realmName}` }, + schema: [{ name: 'Dog', properties: { name: 'string' } }], + }; - Realm.open(config) - .then(realm => { - let actualObjectsCount = realm.objects('Dog').length; - TestCase.assertEqual(actualObjectsCount, expectedObjectsCount, "Synced realm does not contain the expected objects count"); + Realm.open(config) + .then(realm => { + let actualObjectsCount = realm.objects('Dog').length; + TestCase.assertEqual(actualObjectsCount, expectedObjectsCount, "Synced realm does not contain the expected objects count"); - const 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.assertEqual(session.state, 'active'); - resolve(); - }).catch(e => { reject(e) }); - }); + const 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.assertEqual(session.state, 'active'); + resolve(); + }).catch(e => { reject(e) }); }); }); + }); }, testRealmOpenAsync() { @@ -180,7 +180,6 @@ module.exports = { } const username = uuid(); - const username2 = uuid(); const realmName = uuid(); const expectedObjectsCount = 3; @@ -190,6 +189,7 @@ module.exports = { fs.appendFileSync(tmpFile.fd, content, { encoding: 'utf8' }); return new Promise((resolve, reject) => { + //execute download-api-helper which inserts predefined number of objects into the synced realm. const child = execFile('node', [tmpFile.name, username, realmName, REALM_MODULE_PATH], { cwd: tmpDir.name }, (error, stdout, stderr) => { if (error) { reject(new Error('Error executing download api helper' + error)); From b100e782cbec955acf6b565234a994d29073d5b5 Mon Sep 17 00:00:00 2001 From: blagoev Date: Tue, 16 May 2017 12:11:23 +0300 Subject: [PATCH 15/15] Cleaner error checking --- src/js_realm.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/js_realm.hpp b/src/js_realm.hpp index e7a15242..01d7899a 100644 --- a/src/js_realm.hpp +++ b/src/js_realm.hpp @@ -567,7 +567,7 @@ void RealmClass::wait_for_download_completion(ContextType ctx, FunctionType, EventLoopDispatcher wait_handler([=](std::error_code error_code) { HANDLESCOPE - if (error_code == std::error_code{}) { + if (!error_code) { //success Function::callback(protected_ctx, protected_callback, protected_this, 0, nullptr); }