From feb59ae8597b99a8a690b3ab0bb8f7fa69e6586a Mon Sep 17 00:00:00 2001 From: Kenneth Geisshirt Date: Mon, 2 Oct 2017 20:29:36 +0200 Subject: [PATCH] Partial sync (#1361) --- CHANGELOG.md | 2 + binding.gyp | 1 + docs/realm.js | 18 ++++- lib/browser/index.js | 1 + lib/extensions.js | 18 +++++ lib/index.d.ts | 5 ++ react-native/android/src/main/jni/Android.mk | 1 + src/RealmJS.xcodeproj/project.pbxproj | 6 ++ src/js_class.hpp | 10 ++- src/js_realm.hpp | 58 +++++++++++++-- src/js_sync.hpp | 10 ++- tests/js/session-tests.js | 77 +++++++++++++++++++- 12 files changed, 192 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1631daeb..2877d133 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ X.Y.Z-rc Release notes ### Enhancements * Support migration from Realms sync 1.0 to sync 2.0 versions * Handling of the situation when the client has to reset the Realm due to diverging histories (#795). +* Added `Realm.subscribeToObjects()` to listen for changes in partially synced Realms. ### Bug fixes * None @@ -16,6 +17,7 @@ X.Y.Z-rc Release notes * Upgtading to Realm Core 4.0.1 (bug fixes) * Upgrading to Realm Sync 2.0.0-rc26 (sync protocol 22 + bug fixes) + 2.0.0-rc14 Release notes (2017-9-29) ============================================================= ### Breaking changes diff --git a/binding.gyp b/binding.gyp index b4634166..a42caeca 100644 --- a/binding.gyp +++ b/binding.gyp @@ -93,6 +93,7 @@ "src/object-store/src/sync/sync_user.cpp", "src/object-store/src/sync/sync_session.cpp", "src/object-store/src/sync/sync_config.cpp", + "src/object-store/src/sync/partial_sync.cpp", "src/object-store/src/sync/impl/sync_file.cpp", "src/object-store/src/sync/impl/sync_metadata.cpp" ], diff --git a/docs/realm.js b/docs/realm.js index 279f9ee0..4e52ba6f 100644 --- a/docs/realm.js +++ b/docs/realm.js @@ -216,7 +216,7 @@ class Realm { */ cancelTransaction() {} - /* + /** * Replaces all string columns in this Realm with a string enumeration column and compacts the * database file. * @@ -233,6 +233,17 @@ class Realm { * @returns {true} if compaction succeeds. */ compact() {} + + /** + * If the Realm is a partially synchronized Realm, fetch and synchronize the objects + * of a given object type that match the given query (in string format). + * + * **Partial synchronization is a tech preview. Its APIs are subject to change.** + * @param {Realm~ObjectType} type - The type of Realm objects to retrieve. + * @param {string} query - Query used to filter objects. + * @return {Promise} - a promise that will be resolved with the Realm.Results instance when it's available. + */ + subscribeToObjects(className, query, callback) {} } /** @@ -333,7 +344,10 @@ Realm.defaultPath; * The purpose of open_ssl_verify_callback is to enable custom certificate handling and to solve cases where * OpenSSL erroneously rejects valid certificates possibly because OpenSSL doesn't have access to the * proper trust certificates. - * + * - `partial` - Whether this Realm should be opened in 'partial synchronization' mode. + * Partial synchronisation only synchronizes those objects that match the query specified in contrast + * to the normal mode of operation that synchronises all objects in a remote Realm. + * **Partial synchronization is a tech preview. Its APIs are subject to change.** */ /** diff --git a/lib/browser/index.js b/lib/browser/index.js index 8ef9fc12..cc6e7e27 100644 --- a/lib/browser/index.js +++ b/lib/browser/index.js @@ -60,6 +60,7 @@ function setupRealm(realm, realmId) { 'schemaVersion', 'syncSession', 'isInTransaction', + 'subscribeToObjects', ].forEach((name) => { Object.defineProperty(realm, name, {get: util.getterForProperty(name)}); }); diff --git a/lib/extensions.js b/lib/extensions.js index e89b9f53..7a0fd01c 100644 --- a/lib/extensions.js +++ b/lib/extensions.js @@ -141,6 +141,24 @@ module.exports = function(realmConstructor) { //enable deprecated setAccessToken realmConstructor.Sync.setAccessToken = realmConstructor.Sync.setFeatureToken; } + + + // instance methods + Object.defineProperties(realmConstructor.prototype, { + subscribeToObjects: function(objectType, query) { + const realm = this; + let promise = new Promise((resolve, reject) => { + callback = function(results, err) { + if (err) { + reject(err); + } else { + resolve(results); + } + }; + realm._subscriibeToObjects(objectType, query, callback); + }); + } + }); } // TODO: Remove this now useless object. diff --git a/lib/index.d.ts b/lib/index.d.ts index 0d933de4..8c74ed11 100644 --- a/lib/index.d.ts +++ b/lib/index.d.ts @@ -600,6 +600,11 @@ declare class Realm { * @returns boolean */ compact(): boolean; + + /** + * @returns Promise> + */ + subscribeToObjects(objectType: string, query: string): Promise>; } declare module 'realm' { diff --git a/react-native/android/src/main/jni/Android.mk b/react-native/android/src/main/jni/Android.mk index 60d6c30d..dfbd3b84 100644 --- a/react-native/android/src/main/jni/Android.mk +++ b/react-native/android/src/main/jni/Android.mk @@ -59,6 +59,7 @@ LOCAL_SRC_FILES += src/object-store/src/sync/sync_manager.cpp LOCAL_SRC_FILES += src/object-store/src/sync/sync_session.cpp LOCAL_SRC_FILES += src/object-store/src/sync/sync_user.cpp LOCAL_SRC_FILES += src/object-store/src/sync/sync_config.cpp +LOCAL_SRC_FILES += src/object-store/src/sync/partial_sync.cpp LOCAL_SRC_FILES += src/object-store/src/sync/impl/sync_file.cpp LOCAL_SRC_FILES += src/object-store/src/sync/impl/sync_metadata.cpp endif diff --git a/src/RealmJS.xcodeproj/project.pbxproj b/src/RealmJS.xcodeproj/project.pbxproj index c10d5eb2..ec260665 100644 --- a/src/RealmJS.xcodeproj/project.pbxproj +++ b/src/RealmJS.xcodeproj/project.pbxproj @@ -43,6 +43,7 @@ 3FCE2A8B1F58BDEF00D4855B /* primitive_list_notifier.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 3FCE2A891F58BDE500D4855B /* primitive_list_notifier.cpp */; }; 3FCE2A931F58BE0300D4855B /* uuid.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 3FCE2A911F58BDFF00D4855B /* uuid.cpp */; }; 3FCE2A971F58BE2200D4855B /* sync_permission.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 3FCE2A951F58BE1D00D4855B /* sync_permission.cpp */; }; + 420FB79F1F7FBFE900D43D0F /* partial_sync.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 423737AF1F7E333400FAEDFF /* partial_sync.cpp */; }; 502B07E41E2CD201007A84ED /* object.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 502B07E31E2CD1FA007A84ED /* object.cpp */; }; 504CF85E1EBCAE3600A9A4B6 /* network_reachability_observer.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 504CF8541EBCAE3600A9A4B6 /* network_reachability_observer.cpp */; }; 504CF85F1EBCAE3600A9A4B6 /* system_configuration.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 504CF8561EBCAE3600A9A4B6 /* system_configuration.cpp */; }; @@ -194,6 +195,8 @@ 3FCE2A951F58BE1D00D4855B /* sync_permission.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = sync_permission.cpp; path = src/sync/sync_permission.cpp; sourceTree = ""; }; 3FCE2A981F58BE3600D4855B /* descriptor_ordering.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; name = descriptor_ordering.hpp; path = "object-store/src/descriptor_ordering.hpp"; sourceTree = SOURCE_ROOT; }; 3FCE2A991F58BE3600D4855B /* feature_checks.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; name = feature_checks.hpp; path = "object-store/src/feature_checks.hpp"; sourceTree = SOURCE_ROOT; }; + 423737AF1F7E333400FAEDFF /* partial_sync.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = partial_sync.cpp; path = src/sync/partial_sync.cpp; sourceTree = ""; }; + 423737B01F7E333400FAEDFF /* partial_sync.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; name = partial_sync.hpp; path = src/sync/partial_sync.hpp; sourceTree = ""; }; 426FCDFF1F7DA2F9005565DC /* sync_config.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = sync_config.cpp; path = src/sync/sync_config.cpp; sourceTree = ""; }; 502B07E31E2CD1FA007A84ED /* object.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = object.cpp; path = src/object.cpp; sourceTree = ""; }; 502B07E51E2CD20D007A84ED /* object.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; name = object.hpp; path = src/object.hpp; sourceTree = ""; }; @@ -464,6 +467,8 @@ 02E315CC1DB80DE000555337 /* sync */ = { isa = PBXGroup; children = ( + 423737AF1F7E333400FAEDFF /* partial_sync.cpp */, + 423737B01F7E333400FAEDFF /* partial_sync.hpp */, 426FCDFF1F7DA2F9005565DC /* sync_config.cpp */, 504CF8521EBCAE3600A9A4B6 /* impl */, 02E315CD1DB80DF200555337 /* sync_client.hpp */, @@ -936,6 +941,7 @@ 5D97DC4E1F7DAB1400B856A4 /* sync_config.cpp in Sources */, 504CF8601EBCAE3600A9A4B6 /* sync_file.cpp in Sources */, 02E315D21DB80DF200555337 /* sync_file.cpp in Sources */, + 420FB79F1F7FBFE900D43D0F /* partial_sync.cpp in Sources */, 02E315C91DB80DDD00555337 /* sync_manager.cpp in Sources */, 504CF8611EBCAE3600A9A4B6 /* sync_metadata.cpp in Sources */, 02E315D31DB80DF200555337 /* sync_metadata.cpp in Sources */, diff --git a/src/js_class.hpp b/src/js_class.hpp index e7bd7d43..761838d5 100644 --- a/src/js_class.hpp +++ b/src/js_class.hpp @@ -51,6 +51,12 @@ struct Arguments { throw std::invalid_argument(util::format("Invalid arguments: at most %1 expected, but %2 supplied.", max, count)); } } + + void validate_count(size_t actual) const { + if (count != actual) { + throw std::invalid_argument(util::format("Invalid arguments: %1 expected, but %s supplied.", actual, count)); + } + } }; template @@ -60,7 +66,7 @@ template struct PropertyType { using GetterType = void(typename T::Context, typename T::Object, ReturnValue &); using SetterType = void(typename T::Context, typename T::Object, typename T::Value); - + typename T::PropertyGetterCallback getter; typename T::PropertySetterCallback setter; }; @@ -95,7 +101,7 @@ template struct ClassDefinition { using Internal = U; using Parent = V; - + // Every subclass *must* at least have a name. // std::string const name; diff --git a/src/js_realm.hpp b/src/js_realm.hpp index f8454d0b..672deaf7 100644 --- a/src/js_realm.hpp +++ b/src/js_realm.hpp @@ -34,12 +34,14 @@ #include "js_sync.hpp" #include "sync/sync_config.hpp" #include "sync/sync_manager.hpp" +#include "sync/partial_sync.hpp" #endif #include "shared_realm.hpp" #include "binding_context.hpp" #include "object_accessor.hpp" #include "platform.hpp" +#include "results.hpp" namespace realm { namespace js { @@ -156,7 +158,7 @@ class RealmClass : public ClassDefinition> { public: using ObjectDefaultsMap = typename Schema::ObjectDefaultsMap; using ConstructorMap = typename Schema::ConstructorMap; - + using WaitHandler = void(std::error_code); using ProgressHandler = void(uint64_t transferred_bytes, uint64_t transferrable_bytes); @@ -180,6 +182,9 @@ public: static void close(ContextType, ObjectType, Arguments, ReturnValue &); static void compact(ContextType, ObjectType, Arguments, ReturnValue &); static void delete_model(ContextType, ObjectType, Arguments, ReturnValue &); +#if REALM_ENABLE_SYNC + static void subscribe_to_objects(ContextType, ObjectType, Arguments, ReturnValue &); +#endif // properties static void get_empty(ContextType, ObjectType, ReturnValue &); @@ -236,6 +241,9 @@ public: {"close", wrap}, {"compact", wrap}, {"deleteModel", wrap}, + #if REALM_ENABLE_SYNC + {"_subscribeToObjects", wrap}, + #endif }; PropertyMap const properties = { @@ -401,7 +409,7 @@ void RealmClass::constructor(ContextType ctx, ObjectType this_object, size_t else if (config.path.empty()) { config.path = js::default_path(); } - + static const String in_memory_string = "inMemory"; ValueType in_memory_value = Object::get_property(ctx, object, in_memory_string); if (!Value::is_undefined(ctx, in_memory_value) && Value::validated_to_boolean(ctx, in_memory_value, "inMemory")) { @@ -691,14 +699,14 @@ void RealmClass::wait_for_download_completion(ContextType ctx, ObjectType thi auto encryption_key = Value::validated_to_binary(ctx, encryption_key_value, "encryptionKey"); config.encryption_key.assign(encryption_key.data(), encryption_key.data() + encryption_key.size()); } - + Protected thiz(ctx, this_object); SyncClass::populate_sync_config(ctx, thiz, config_object, config); Protected protected_callback(ctx, callback_function); Protected protected_this(ctx, this_object); Protected protected_ctx(Context::get_global_context(ctx)); - + EventLoopDispatcher wait_handler([=](std::error_code error_code) { HANDLESCOPE if (!error_code) { @@ -718,7 +726,7 @@ void RealmClass::wait_for_download_completion(ContextType ctx, ObjectType thi }); std::function waitFunc = std::move(wait_handler); - std::function progressFunc; + std::function progressFunc; SharedRealm realm; try { @@ -771,8 +779,8 @@ void RealmClass::wait_for_download_completion(ContextType ctx, ObjectType thi if (progressFuncDefined) { session->register_progress_notifier(std::move(progressFunc), SyncSession::NotifierType::download, false); - } - + } + session->wait_for_download_completion([=](std::error_code error_code) { realm->close(); //capture and keep realm instance for until here waitFunc(error_code); @@ -1016,5 +1024,41 @@ void RealmClass::compact(ContextType ctx, ObjectType this_object, Arguments a return_value.set(realm->compact()); } +#if REALM_ENABLE_SYNC +template +void RealmClass::subscribe_to_objects(ContextType ctx, ObjectType this_object, Arguments args, ReturnValue &return_value) { + args.validate_count(3); + + SharedRealm realm = *get_internal>(this_object); + auto class_name = Value::validated_to_string(ctx, args[0]); + auto query = Value::validated_to_string(ctx, args[1]); + auto callback = Value::validated_to_function(ctx, args[2]); + + Protected protected_this(ctx, this_object); + Protected protected_ctx(Context::get_global_context(ctx)); + auto cb = [=](realm::Results results, std::exception_ptr err) { + if (err) { + try { + rethrow_exception(err); + } + catch (const std::exception& e) { + typename T::Value arguments[2]; + arguments[0] = Value::from_null(protected_ctx); + arguments[1] = Value::from_string(protected_ctx, e.what()); + Function::callback(ctx, callback, protected_this, 2, arguments); + } + return; + } + + typename T::Value arguments[2]; + arguments[0] = ResultsClass::create_instance(protected_ctx, results); + arguments[1] = Value::from_null(protected_ctx); + Function::callback(protected_ctx, callback, protected_this, 2, arguments); + }; + + partial_sync::register_query(realm, class_name, query, std::move(cb)); +} +#endif + } // js } // realm diff --git a/src/js_sync.hpp b/src/js_sync.hpp index 6c08e9ee..14403170 100644 --- a/src/js_sync.hpp +++ b/src/js_sync.hpp @@ -653,14 +653,20 @@ void SyncClass::populate_sync_config(ContextType ctx, ObjectType realm_constr ssl_verify_callback = std::move(ssl_verify_functor); } + bool is_partial = false; + ValueType partial_value = Object::get_property(ctx, sync_config_object, "partial"); + if (!Value::is_undefined(ctx, partial_value)) { + is_partial = Value::validated_to_boolean(ctx, partial_value); + } + // FIXME - use make_shared config.sync_config = std::shared_ptr(new SyncConfig{shared_user, raw_realm_url, SyncSessionStopPolicy::AfterChangesUploaded, std::move(bind), std::move(error_handler), nullptr, util::none, client_validate_ssl, ssl_trust_certificate_path, - std::move(ssl_verify_callback)}); - + std::move(ssl_verify_callback), + is_partial}); config.schema_mode = SchemaMode::Additive; diff --git a/tests/js/session-tests.js b/tests/js/session-tests.js index 7e4e0f8a..b58e7388 100644 --- a/tests/js/session-tests.js +++ b/tests/js/session-tests.js @@ -493,7 +493,7 @@ module.exports = { }; Realm.open(config) - .then(realm => + .then(realm => _reject("Should fail with IncompatibleSyncedRealmError")) .catch(e => { if (e.name == "IncompatibleSyncedRealmError") { @@ -502,7 +502,7 @@ module.exports = { resolve(); return; } - + function printObject(o) { var out = ''; for (var p in o) { @@ -781,6 +781,79 @@ module.exports = { }); }, + testPartialSync() { + // FIXME: try to enable for React Native + if (!isNodeProccess) { + return Promise.resolve(); + } + + const username = uuid(); + const realmName = uuid(); + + return runOutOfProcess(__dirname + '/download-api-helper.js', username, realmName, REALM_MODULE_PATH) + .then(() => { + Realm.Sync.User.login('http://localhost:9080', username, 'password').then(user1 => { + TestCase.assertDefined(user1, 'user1'); + let config1 = { + sync: { + user: user1, + url: `realm://localhost:9080/~/${realmName}`, + }, + schema: [{ name: 'Integer', properties: { value: 'int' } }], + }; + return new Promise((resolve, reject) => { + return Realm.open(config1) + .then(realm1 => { + for(let i = 0; i < 10; i++) { + realm1.write(() => { + realm1.create('Integer', {value: i}); + }); + } + + const progressCallback = (transferred, total) => { + if (transferred === total) { + resolve(); + } + } + realm.syncSession.addProgressNotification('upload', 'reportIndefinitely', progressCallback); + }) + .then(() => { + realm.close(); + realm.deleteFile(config1); + user1.logout(); + }); + }); + }) + }) + .then(() => { + Realm.Sync.User.login('http://localhost:9080', username, 'password').then(user2 => { + TestCase.assertDefined(user2, 'user2'); + return new Promise((resolve, reject) => { + let config2 = { + sync: { + user: user2, + url: `realm://localhost:9080/~/${realmName}`, + partial: true, + }, + schema: [{ name: 'Integer', properties: { value: 'int' } }], + }; + + const realm2 = new Realm(config2); + realm2.subscribeToObjects('Integer', 'value > 5').then((results, error) => { + return results; + }).then((results) => { + TestCase.assertEqual(results.length, 4); + for(obj in results) { + TestCase.assertTrue(obj.value > 5, '<= 5'); + } + resolve(); + }); + reject(); + }) + }) + }) + }, + testClientReset() { // FIXME: try to enable for React Native if (!isNodeProccess) {