diff --git a/lib/extensions.js b/lib/extensions.js index 7b086554..83ee4542 100644 --- a/lib/extensions.js +++ b/lib/extensions.js @@ -43,26 +43,53 @@ module.exports = function(realmConstructor) { //Add async open API Object.defineProperties(realmConstructor, getOwnPropertyDescriptors({ open(config) { - return new Promise((resolve, reject) => { - realmConstructor._waitForDownload(config, (error) => { - if (error) { - reject(error); - } - else { - try { - let syncedRealm = new this(config); - //FIXME: RN hangs here. Remove when node's makeCallback alternative is implemented - setTimeout(() => { resolve(syncedRealm); }, 1); - } catch (e) { - reject(e); + let syncSession; + let promise = new Promise((resolve, reject) => { + realmConstructor._waitForDownload(config, + (session) => { + syncSession = session; + }, + (error) => { + if (error) { + reject(error); } - } - }); + else { + try { + let syncedRealm = new this(config); + //FIXME: RN hangs here. Remove when node's makeCallback alternative is implemented + setTimeout(() => { resolve(syncedRealm); }, 1); + } catch (e) { + reject(e); + } + } + }); }); + + promise.progress = (callback) => { + if (syncSession) { + syncSession.addProgressNotification('download', 'forCurrentlyOutstandingWork', callback); + } + + return promise; + }; + + return promise; }, - openAsync(config, callback) { - realmConstructor._waitForDownload(config, (error) => { + openAsync(config, progressCallback, callback) { + + if (!callback) { + callback = progressCallback; + progressCallback = null; + } + + realmConstructor._waitForDownload(config, + (syncSession) => { + if (progressCallback) { + syncSession.addProgressNotification('download', 'forCurrentlyOutstandingWork', progressCallback);; + } + }, + (error) => { if (error) { callback(error); } diff --git a/src/js_realm.hpp b/src/js_realm.hpp index 848a2ccc..7d3844e4 100644 --- a/src/js_realm.hpp +++ b/src/js_realm.hpp @@ -619,9 +619,14 @@ 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, 2); + validate_argument_count(argc, 2, 3); auto config_object = Value::validated_to_object(ctx, arguments[0]); - auto callback_function = Value::validated_to_function(ctx, arguments[1]); + auto callback_function = Value::validated_to_function(ctx, arguments[argc - 1]); + + FunctionType session_callback; + if (argc == 3) { + session_callback = Value::validated_to_function(ctx, arguments[1]); + } #if REALM_ENABLE_SYNC ValueType sync_config_value = Object::get_property(ctx, config_object, "sync"); @@ -694,6 +699,13 @@ void RealmClass::wait_for_download_completion(ContextType ctx, FunctionType, 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 (Value::is_valid(session_callback)) { + auto syncSession = create_object>(ctx, new WeakSession(session)); + ValueType callback_arguments[1]; + callback_arguments[0] = syncSession; + Function::callback(protected_ctx, session_callback, protected_this, 1, callback_arguments); + } + if (progressFuncDefined) { session->register_progress_notifier(std::move(progressFunc), SyncSession::NotifierType::download, false); } diff --git a/src/js_sync.hpp b/src/js_sync.hpp index 51725789..204d6fe6 100644 --- a/src/js_sync.hpp +++ b/src/js_sync.hpp @@ -147,6 +147,31 @@ void UserClass::logout(ContextType ctx, FunctionType, ObjectType this_object, get_internal>(this_object)->get()->log_out(); } +template +class ProgressNotificationTokenClass : public ClassDefinition { + using GlobalContextType = typename T::GlobalContext; + using ContextType = typename T::Context; + using FunctionType = typename T::Function; + using ObjectType = typename T::Object; + using ValueType = typename T::Value; + using String = js::String; + using Object = js::Object; + using Value = js::Value; + using Function = js::Function; + using ReturnValue = js::ReturnValue; + +public: + std::string const name = "ProgressNotificationToken"; + + static ObjectType create_instance(ContextType, uint64_t); + + static void stop(ContextType, FunctionType, ObjectType, size_t, const ValueType[], ReturnValue &); + + MethodMap const methods = { + {"stop", wrap}, + }; +}; + template class SessionClass : public ClassDefinition { using ContextType = typename T::Context; @@ -160,6 +185,7 @@ class SessionClass : public ClassDefinition { public: std::string const name = "Session"; + using ProgressHandler = void(uint64_t transferred_bytes, uint64_t transferrable_bytes); static FunctionType create_constructor(ContextType); @@ -170,6 +196,10 @@ public: static void simulate_error(ContextType, FunctionType, ObjectType, size_t, const ValueType[], ReturnValue &); static void refresh_access_token(ContextType, FunctionType, ObjectType, size_t, const ValueType[], ReturnValue &); +#if REALM_ENABLE_SYNC + static void add_progress_notification(ContextType ctx, FunctionType, ObjectType this_object, size_t argc, const ValueType arguments[], ReturnValue &); + static void remove_progress_notification(ContextType ctx, FunctionType, ObjectType this_object, size_t argc, const ValueType arguments[], ReturnValue &); +#endif PropertyMap const properties = { {"config", {wrap, nullptr}}, @@ -180,7 +210,11 @@ public: MethodMap const methods = { {"_simulateError", wrap}, - {"_refreshAccessToken", wrap} + {"_refreshAccessToken", wrap}, +#if REALM_ENABLE_SYNC + {"addProgressNotification", wrap}, + {"removeProgressNotification", wrap}, +#endif }; }; @@ -307,8 +341,82 @@ void SessionClass::refresh_access_token(ContextType ctx, FunctionType, Object } } +#if REALM_ENABLE_SYNC template -class SyncClass : public ClassDefinition { +void SessionClass::add_progress_notification(ContextType ctx, FunctionType, ObjectType this_object, size_t argc, const ValueType arguments[], ReturnValue &return_value) { + validate_argument_count(argc, 3); + + if (auto session = get_internal>(this_object)->lock()) { + + std::string direction = Value::validated_to_string(ctx, arguments[0], "direction"); + std::string mode = Value::validated_to_string(ctx, arguments[1], "mode"); + SyncSession::NotifierType notifierType; + if (direction == "download") { + notifierType = SyncSession::NotifierType::download; + } + else if (direction == "upload") { + notifierType = SyncSession::NotifierType::upload; + } + else { + throw std::invalid_argument("Invalid argument 'direction'. Only 'download' and 'upload' progress notification directions are supported"); + } + + bool is_streaming = false; + if (mode == "reportIndefinitely") { + is_streaming = true; + } + else if (mode == "forCurrentlyOutstandingWork") { + is_streaming = false; + } + else { + throw std::invalid_argument("Invalid argument 'mode'. Only 'reportIndefinitely' and 'forCurrentlyOutstandingWork' progress notification modes are supported"); + } + + auto callback_function = Value::validated_to_function(ctx, arguments[2], "callback"); + + Protected protected_callback(ctx, callback_function); + Protected protected_this(ctx, this_object); + Protected protected_ctx(Context::get_global_context(ctx)); + std::function progressFunc; + + EventLoopDispatcher progress_handler([=](uint64_t transferred_bytes, uint64_t transferrable_bytes) { + HANDLESCOPE + ValueType callback_arguments[2]; + callback_arguments[0] = Value::from_number(protected_ctx, transferred_bytes); + callback_arguments[1] = Value::from_number(protected_ctx, transferrable_bytes); + + Function::callback(protected_ctx, protected_callback, protected_this, 2, callback_arguments); + }); + + progressFunc = std::move(progress_handler); + + + auto registrationToken = session->register_progress_notifier(std::move(progressFunc), notifierType, false); + + auto syncSession = create_object>(ctx, new WeakSession(session)); + PropertyAttributes attributes = ReadOnly | DontEnum | DontDelete; + Object::set_property(ctx, callback_function, "syncSession", syncSession, attributes); + Object::set_property(ctx, callback_function, "registrationToken", Value::from_number(protected_ctx, registrationToken), attributes); + } +} + +template +void SessionClass::remove_progress_notification(ContextType ctx, FunctionType, ObjectType this_object, size_t argc, const ValueType arguments[], ReturnValue &return_value) { + validate_argument_count(argc, 1); + auto callback_function = Value::validated_to_function(ctx, arguments[0], "callback"); + auto syncSessionProp = Object::get_property(ctx, callback_function, "syncSession"); + auto syncSession = Value::validated_to_object(ctx, syncSessionProp); + auto registrationToken = Object::get_property(ctx, callback_function, "registrationToken"); + + if (auto session = get_internal>(syncSession)->lock()) { + auto reg = Value::validated_to_number(ctx, registrationToken); + session->unregister_progress_notifier(reg); + } +} +#endif + +template +class SyncClass : public ClassDefinition { using GlobalContextType = typename T::GlobalContext; using ContextType = typename T::Context; using FunctionType = typename T::Function; @@ -447,5 +555,13 @@ void SyncClass::populate_sync_config(ContextType ctx, ObjectType realm_constr } } +template +void ProgressNotificationTokenClass::stop(ContextType ctx, FunctionType, ObjectType this_object, size_t argc, const ValueType arguments[], ReturnValue &return_value) { +} + +template +typename T::Object ProgressNotificationTokenClass::create_instance(ContextType ctx, uint64_t progressToken) { + return create_object>(ctx, new uint64_t(progressToken)); +} } // js } // realm diff --git a/tests/js/session-tests.js b/tests/js/session-tests.js index ed0cb82a..407641e9 100644 --- a/tests/js/session-tests.js +++ b/tests/js/session-tests.js @@ -26,7 +26,7 @@ const Realm = require('realm'); const TestCase = require('./asserts'); const isNodeProccess = (typeof process === 'object' && process + '' === '[object process]'); -console.log("isnode " + isNodeProccess + " typeof " + (typeof(process) === 'object')); + function node_require(module) { return require(module); } @@ -84,13 +84,23 @@ function runOutOfProcess(nodeJsFilePath) { fs.appendFileSync(tmpFile.fd, content, { encoding: 'utf8' }); nodeArgs[0] = tmpFile.name; return new Promise((resolve, reject) => { - const child = execFile('node', nodeArgs, { cwd: tmpDir.name }, (error, stdout, stderr) => { - if (error) { - reject(new Error(`Error executing ${nodeJsFilePath} Error: ${error}`)); + try { + console.log('runOutOfProcess command\n node ' + nodeArgs.join(" ")); + const child = execFile('node', nodeArgs, { cwd: tmpDir.name }, (error, stdout, stderr) => { + if (error) { + console.error("runOutOfProcess failed\n" + error); + reject(new Error(`Running ${nodeJsFilePath} failed. error: ${error}`)); + return; + } + + console.log('runOutOfProcess success\n' + stdout); + resolve(); + }); } - resolve(); - }); - }) + catch (e) { + reject(e); + }; + }); } module.exports = { @@ -150,7 +160,7 @@ module.exports = { const realmName = uuid(); const expectedObjectsCount = 3; - runOutOfProcess(__dirname + '/download-api-helper.js', username, realmName, REALM_MODULE_PATH) + return runOutOfProcess(__dirname + '/download-api-helper.js', username, realmName, REALM_MODULE_PATH) .then(() => { return promisifiedLogin('http://localhost:9080', username, 'password').then(user => { const accessTokenRefreshed = this; @@ -186,7 +196,7 @@ module.exports = { const realmName = uuid(); const expectedObjectsCount = 3; - runOutOfProcess(__dirname + '/download-api-helper.js', username, realmName, REALM_MODULE_PATH) + return runOutOfProcess(__dirname + '/download-api-helper.js', username, realmName, REALM_MODULE_PATH) .then(() => { return promisifiedLogin('http://localhost:9080', username, 'password').then(user => { return new Promise((resolve, reject) => { @@ -239,7 +249,7 @@ module.exports = { const realmName = uuid(); const expectedObjectsCount = 3; - runOutOfProcess(__dirname + '/download-api-helper.js', username, realmName, REALM_MODULE_PATH) + return runOutOfProcess(__dirname + '/download-api-helper.js', username, realmName, REALM_MODULE_PATH) .then(() => { return promisifiedLogin('http://localhost:9080', username, 'password').then(user => { const accessTokenRefreshed = this; @@ -275,7 +285,7 @@ module.exports = { const realmName = uuid(); const expectedObjectsCount = 3; - runOutOfProcess(__dirname + '/download-api-helper.js', username, realmName, REALM_MODULE_PATH) + return runOutOfProcess(__dirname + '/download-api-helper.js', username, realmName, REALM_MODULE_PATH) .then(() => { return promisifiedLogin('http://localhost:9080', username, 'password').then(user => { return new Promise((resolve, reject) => { @@ -322,7 +332,7 @@ module.exports = { const realmName = uuid(); const expectedObjectsCount = 3; - runOutOfProcess(__dirname + '/download-api-helper.js', username, realmName, REALM_MODULE_PATH) + return runOutOfProcess(__dirname + '/download-api-helper.js', username, realmName, REALM_MODULE_PATH) .then(() => { return promisifiedLogin('http://localhost:9080', username, 'password').then(user => { return new Promise((resolve, reject) => { @@ -449,5 +459,191 @@ module.exports = { session._simulateError(123, 'simulated error'); }); }); - } + }, + + + testProgressNotificationsForRealmConstructor() { + if (!isNodeProccess) { + return Promise.resolve(); + } + + const username = uuid(); + const realmName = uuid(); + + return runOutOfProcess(__dirname + '/download-api-helper.js', username, realmName, REALM_MODULE_PATH) + .then(() => { + return promisifiedLogin('http://localhost:9080', username, 'password').then(user => { + return new Promise((resolve, reject) => { + let config = { + sync: { + user, + url: `realm://localhost:9080/~/${realmName}` + }, + schema: [{ name: 'Dog', properties: { name: 'string' } }], + }; + + let realm = new Realm(config); + const progressCallback = (transferred, total) => { + resolve(); + }; + + realm.syncSession.addProgressNotification('download', 'reportIndefinitely', progressCallback); + + setTimeout(function() { + reject("Progress Notifications API failed to call progress callback for Realm constructor"); + }, 5000); + }); + }); + }); + }, + + testProgressNotificationsUnregisterForRealmConstructor() { + if (!isNodeProccess) { + return Promise.resolve(); + } + + const username = uuid(); + const realmName = uuid(); + + return runOutOfProcess(__dirname + '/download-api-helper.js', username, realmName, REALM_MODULE_PATH) + .then(() => { + return promisifiedLogin('http://localhost:9080', username, 'password').then(user => { + return new Promise((resolve, reject) => { + let config = { + sync: { + user, + url: `realm://localhost:9080/~/${realmName}` + }, + schema: [{ name: 'Dog', properties: { name: 'string' } }], + }; + + let realm = new Realm(config); + let unregisterFunc; + + let writeDataFunc = () => { + realm.write(() => { + for (let i = 1; i <= 3; i++) { + realm.create('Dog', { name: `Lassy ${i}` }); + } + }); + } + + let syncFinished = false; + let failOnCall = false; + const progressCallback = (transferred, total) => { + if (failOnCall) { + reject(new Error("Progress callback should not be called after removeProgressNotification")); + } + + syncFinished = transferred === total; + + //unregister and write some new data. + if (syncFinished) { + failOnCall = true; + unregisterFunc(); + + //use second callback to wait for sync finished + realm.syncSession.addProgressNotification('upload', 'reportIndefinitely', (x, y) => { + if (x === y) { + resolve(); + } + }); + writeDataFunc(); + } + }; + + realm.syncSession.addProgressNotification('upload', 'reportIndefinitely', progressCallback); + + unregisterFunc = () => { + realm.syncSession.removeProgressNotification(progressCallback); + }; + + writeDataFunc(); + }); + }); + }); + }, + + testProgressNotificationsForRealmOpen() { + if (!isNodeProccess) { + return Promise.resolve(); + } + + const username = uuid(); + const realmName = uuid(); + + return runOutOfProcess(__dirname + '/download-api-helper.js', username, realmName, REALM_MODULE_PATH) + .then(() => { + return promisifiedLogin('http://localhost:9080', username, 'password').then(user => { + return new Promise((resolve, reject) => { + let config = { + sync: { + user, + url: `realm://localhost:9080/~/${realmName}` + }, + schema: [{ name: 'Dog', properties: { name: 'string' } }], + }; + + let progressCalled = false; + Realm.open(config) + .progress((transferred, total) => { + progressCalled = true; + }) + .then(() => { + TestCase.assertTrue(progressCalled); + resolve(); + }) + .catch((e) => reject(e)); + + setTimeout(function() { + reject("Progress Notifications API failed to call progress callback for Realm constructor"); + }, 5000); + }); + }); + }); + }, + + testProgressNotificationsForRealmOpenAsync() { + if (!isNodeProccess) { + return Promise.resolve(); + } + + const username = uuid(); + const realmName = uuid(); + + return runOutOfProcess(__dirname + '/download-api-helper.js', username, realmName, REALM_MODULE_PATH) + .then(() => { + return promisifiedLogin('http://localhost:9080', username, 'password').then(user => { + return new Promise((resolve, reject) => { + let config = { + sync: { + user, + url: `realm://localhost:9080/~/${realmName}` + }, + schema: [{ name: 'Dog', properties: { name: 'string' } }], + }; + + let progressCalled = false; + + Realm.openAsync(config, + (transferred, total) => { + progressCalled = true; + }, + (error) => { + if (error) { + reject(error); + return; + } + + TestCase.assertTrue(progressCalled); + resolve(); + }); + + setTimeout(function() { + reject("Progress Notifications API failed to call progress callback for Realm constructor"); + }, 5000); + }); + }); + }); + }, }