From 4cb9c77f46eac7f1e9320915ef9d89eb431af270 Mon Sep 17 00:00:00 2001 From: Christian Melchior Date: Wed, 30 May 2018 12:54:51 +0200 Subject: [PATCH] Query-based sync as the default sync mode (#1830) --- CHANGELOG.md | 26 +++++++++++++ README.md | 7 ++++ docs/jsdoc-template | 2 +- docs/realm.js | 1 + docs/sync.js | 19 ++++++++-- lib/extensions.js | 1 - lib/index.d.ts | 16 ++++++-- lib/permission-api.js | 3 +- lib/user-methods.js | 38 +++++++++++++++++++ package.json | 1 + src/js_sync.hpp | 21 ++++++++--- tests/js/object-id-tests.js | 4 +- tests/js/partial-sync-api-helper.js | 2 +- tests/js/permission-tests.js | 6 +-- tests/js/session-tests.js | 26 ++++++------- tests/js/user-tests.js | 57 ++++++++++++++++++++++++++++- 16 files changed, 193 insertions(+), 37 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9daf1cb7..519af51a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,29 @@ +2.8.0 Release notes (YYYY-MM-DD) +============================================================= +### Compatibility +* Sync protocol: 24 +* Server-side history format: 4 +* File format: 7 +* Realm Object Server: 3.0.0 or later + +The feature known as Partial synchronization has been renamed to Query-based synchronization and is now the default mode +for synchronized Realms. This has impacted a number of APIs. See below for the details. + +### Deprecated + +* [Sync] `Realm.Configuration.SyncConfiguration.partial` has been deprecated in favor of `Realm.Configuration.SyncConfiguration.fullSynchronization`. +* [Sync] `Realm.automaticSyncConfiguration()` has been deprecated in favor of `Realm.Sync.User.createConfiguration()` + +### Enhancements + +* [Sync] `Realm.Configuration.SyncConfiguration.fullSynchronization` has been added. +* [Sync] `Realm.Sync.User.createConfiguration(config)` has been added for creating default and user defined sync configurations. + +### Internal + +* [Sync] `Realm.Configuration.SyncConfig._disablePartialSyncUrlChecks` has been renamed to `Realm.Configuration.sync._disableQueryBasedSyncUrlChecks`. + + 2.7.0 Release notes (2018-5-29) ============================================================= ### Compatibility diff --git a/README.md b/README.md index 7b150b3b..4c3abbbc 100644 --- a/README.md +++ b/README.md @@ -83,6 +83,13 @@ npm install --build-from-source=realm - Check [node-gyp](https://github.com/nodejs/node-gyp) manual for custom installation procedure for Windows +### Building docs: +API documentation is written using [JSDoc](http://usejsdoc.org/). + +- `npm run jsdoc` + +The generated docs can be found by opening `docs/output/realm//index.html`. + ## Debugging the node addon You can use (Visual Studio Code)[https://code.visualstudio.com/] to develop and debug. In the `.vscode` folder, configuration for building and debugging has been added for your convience. diff --git a/docs/jsdoc-template b/docs/jsdoc-template index 682e21fe..441d8a45 160000 --- a/docs/jsdoc-template +++ b/docs/jsdoc-template @@ -1 +1 @@ -Subproject commit 682e21fe456b39169ff2d4f3f7ffc24d04cb84fd +Subproject commit 441d8a45eb39d454ea612251bf2adb60473ede70 diff --git a/docs/realm.js b/docs/realm.js index c26edc2e..e6eb215d 100644 --- a/docs/realm.js +++ b/docs/realm.js @@ -128,6 +128,7 @@ class Realm { * @throws {Error} if zero or multiple users are logged in * @returns {Realm~Configuration} - a configuration matching a default synced Realm. * @since 2.3.0 + * @deprecated use {@link Sync.User.createConfiguration()} instead. */ static automaticSyncConfiguration(user) {} diff --git a/docs/sync.js b/docs/sync.js index 07c0b120..0adcea75 100644 --- a/docs/sync.js +++ b/docs/sync.js @@ -35,10 +35,14 @@ * accept or reject the server's SSL certificate. * * @property {Realm.Sync~SSLConfiguration} [ssl] - SSL configuration. - * @property {boolean} [partial] - Whether this Realm should be opened in 'partial synchronization' mode. - * Partial synchronisation only synchronizes those objects that match the query specified in contrast + * @deprecated + * @property {boolean} [partial] - Whether this Realm should be opened in 'query-based synchronization' mode. + * Query-based 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.** + * @property {boolean} [fullSynchronization] - Whether this Realm should be opened in query-based or full + * synchronization mode. The default is query-based mode which only synchronizes objects that have been subscribed to. + * A fully synchronized Realm will synchronize the entire Realm in the background, irrespectively of the data being + * used or not. */ /** @@ -438,6 +442,15 @@ class User { */ get isAdminToken() {} + /** + * Creates the configuration object required to open a synchronized Realm. + * + * @param {Realm.PartialConfiguration} config - optional parameters that should override any default settings. + * @returns {Realm.Configuration} the full Realm configuration + * @since 3.0.0 + */ + createConfiguration(config) {} + /** * Logs out the user from the Realm Object Server. */ diff --git a/lib/extensions.js b/lib/extensions.js index 691ea757..bbd5ce4b 100644 --- a/lib/extensions.js +++ b/lib/extensions.js @@ -181,7 +181,6 @@ module.exports = function(realmConstructor) { sync: { user: user, url: realmUrl, - partial: true } }; return config; diff --git a/lib/index.d.ts b/lib/index.d.ts index d89e0958..3cd32948 100644 --- a/lib/index.d.ts +++ b/lib/index.d.ts @@ -83,11 +83,18 @@ declare namespace Realm { inMemory?: boolean; schema?: (ObjectClass | ObjectSchema)[]; schemaVersion?: number; - sync?: Realm.Sync.SyncConfiguration; + sync?: Partial; deleteRealmIfMigrationNeeded?: boolean; disableFormatUpgrade?: boolean; } + /** + * realm configuration used for overriding default configuration values. + * @see { @link https://realm.io/docs/javascript/latest/api/Realm.html#~Configuration } + */ + interface PartialConfiguration extends Partial { + } + // object props type interface ObjectPropsType { [keys: string]: any; @@ -314,6 +321,7 @@ declare namespace Realm.Sync { static confirmEmail(server:string, confirmation_token:string): Promise; + createConfiguration(config?: Realm.PartialConfiguration): Realm.Configuration authenticate(server: string, provider: string, options: any): Promise; logout(): void; openManagementRealm(): Realm; @@ -415,7 +423,8 @@ declare namespace Realm.Sync { ssl?: SSLConfiguration; error?: ErrorCallback; partial?: boolean; - _disablePartialSyncUrlChecks?:boolean; + fullSynchronization?: boolean; + _disableQueryBasedSyncUrlChecks?:boolean; } type ProgressNotificationCallback = (transferred: number, transferable: number) => void; @@ -588,8 +597,6 @@ declare class Realm { */ static schemaVersion(path: string, encryptionKey?: ArrayBuffer | ArrayBufferView): number; - - /** * Open a realm asynchronously with a promise. If the realm is synced, it will be fully synchronized before it is available. * @param {Configuration} config @@ -605,6 +612,7 @@ declare class Realm { static openAsync(config: Realm.Configuration, callback: (error: any, realm: Realm) => void, progressCallback?: Realm.Sync.ProgressNotificationCallback): void /** + * @deprecated in favor of `Realm.Sync.User.createConfiguration()`. * Return a configuration for a default Realm. * @param {Realm.Sync.User} optional user. */ diff --git a/lib/permission-api.js b/lib/permission-api.js index 71caf39b..8a75ac7f 100644 --- a/lib/permission-api.js +++ b/lib/permission-api.js @@ -68,7 +68,8 @@ function getSpecialPurposeRealm(user, realmName, schema) { schema: schema, sync: { user, - url: url.href + url: url.href, + fullSynchronization: true } }; diff --git a/lib/user-methods.js b/lib/user-methods.js index f83b2a86..f1bbce2c 100644 --- a/lib/user-methods.js +++ b/lib/user-methods.js @@ -21,7 +21,9 @@ const AuthError = require('./errors').AuthError; const permissionApis = require('./permission-api'); +const merge = require('deepmerge'); const require_method = require; +const URL = require('url-parse'); function node_require(module) { return require_method(module); @@ -520,6 +522,42 @@ const instanceMethods = { } }); }, + createConfiguration(config) { + + if (config && config.sync) { + if (console.warn !== undefined) { + console.warn(`'user' property will be overridden by ${this.identity}`); + } + if (config.sync.partial !== undefined && config.sync.fullSynchronization !== undefined) { + throw new Error("'partial' and 'fullSynchronization' were both set. 'partial' has been deprecated, use only 'fullSynchronization'"); + } + } + + // Create default config + let url = new URL(this.server); + let secure = (url.protocol === 'https:')?'s':''; + let port = (url.port === undefined)?'9080':url.port; + let realmUrl = `realm${secure}://${url.hostname}:${port}/default`; + + let defaultConfig = { + sync: { + user: this, + url: realmUrl, + } + }; + + // Set query-based as the default setting if the user doesn't specified any other behaviour. + if (!(config && config.sync && config.sync.partial)) { + defaultConfig.sync.fullSynchronization = false; + } + + // Merge default configuration with user provided config. User defined properties should aways win. + // Doing the naive merge in JS break objects that are backed by native objects, so these needs to + // be merged manually. This is currently only `sync.user`. + let mergedConfig = (config === undefined) ? defaultConfig : merge(defaultConfig, config); + mergedConfig.sync.user = this; + return mergedConfig; + }, }; // Append the permission apis diff --git a/package.json b/package.json index 8887cfa4..4a4b2821 100644 --- a/package.json +++ b/package.json @@ -80,6 +80,7 @@ "dependencies": { "command-line-args": "^4.0.6", "decompress": "^4.2.0", + "deepmerge": "2.1.0", "fs-extra": "^4.0.2", "ini": "^1.3.4", "nan": "2.8.0", diff --git a/src/js_sync.hpp b/src/js_sync.hpp index 13cb4ba5..e301c8a7 100644 --- a/src/js_sync.hpp +++ b/src/js_sync.hpp @@ -833,19 +833,28 @@ void SyncClass::populate_sync_config(ContextType ctx, ObjectType realm_constr ssl_verify_callback = std::move(ssl_verify_functor); } - bool is_partial = false; + bool is_partial = false; // Change to `true` when `partial` is removed + ValueType full_synchronization_value = Object::get_property(ctx, sync_config_object, "fullSynchronization"); ValueType partial_value = Object::get_property(ctx, sync_config_object, "partial"); + + // Disallow setting `partial` and `fullSynchronization` at the same time + if (!Value::is_undefined(ctx, full_synchronization_value) && !Value::is_undefined(ctx, partial_value)) { + throw std::invalid_argument("'partial' and 'fullSynchronization' were both set. 'partial' has been deprecated, use only 'fullSynchronization'"); + } + if (!Value::is_undefined(ctx, partial_value)) { is_partial = Value::validated_to_boolean(ctx, partial_value); + } else if (!Value::is_undefined(ctx, full_synchronization_value)) { + is_partial = !Value::validated_to_boolean(ctx, full_synchronization_value); } - bool disable_partial_sync_url_checks = false; - ValueType disable_partial_sync_url_checks_value = Object::get_property(ctx, sync_config_object, "_disablePartialSyncUrlChecks"); - if (!Value::is_undefined(ctx, disable_partial_sync_url_checks_value)) { - disable_partial_sync_url_checks = Value::validated_to_boolean(ctx, disable_partial_sync_url_checks_value); + bool disable_query_based_sync_url_checks = false; + ValueType disable_query_based_sync_url_checks_value = Object::get_property(ctx, sync_config_object, "_disableQueryBasedSyncUrlChecks"); + if (!Value::is_undefined(ctx, disable_query_based_sync_url_checks_value)) { + disable_query_based_sync_url_checks = Value::validated_to_boolean(ctx, disable_query_based_sync_url_checks_value); } - if (disable_partial_sync_url_checks) { + if (disable_query_based_sync_url_checks) { config.sync_config = std::make_shared(shared_user, std::move("")); config.sync_config->reference_realm_url = std::move(raw_realm_url); } diff --git a/tests/js/object-id-tests.js b/tests/js/object-id-tests.js index 1402f8d9..faf778a9 100644 --- a/tests/js/object-id-tests.js +++ b/tests/js/object-id-tests.js @@ -49,12 +49,12 @@ module.exports = { } return Realm.Sync.User.register('http://localhost:9080', uuid(), 'password').then(user => { - const config = { sync: { user, url: 'realm://localhost:9080/~/myrealm' }, + const config = user.createConfiguration({ sync: { url: 'realm://localhost:9080/~/myrealm' }, schema: [{ name: 'IntegerPrimaryKey', properties: { int: 'int?' }, primaryKey: 'int' }, { name: 'StringPrimaryKey', properties: { string: 'string?' }, primaryKey: 'string' }, { name: 'NoPrimaryKey', properties: { string: 'string' }}, ], - }; + }); return Realm.open(config).then(realm => { var integer, nullInteger; var string, nullString; diff --git a/tests/js/partial-sync-api-helper.js b/tests/js/partial-sync-api-helper.js index c0b0bcb4..e64a3bbb 100644 --- a/tests/js/partial-sync-api-helper.js +++ b/tests/js/partial-sync-api-helper.js @@ -32,7 +32,7 @@ function createObjects(user) { sync: { user, url: `realm://localhost:9080/default`, - partial: true, + fullSynchronization: false, error: err => console.log('partial-sync-api-helper', err) }, schema: [{ name: 'Dog', properties: { name: 'string' } }] diff --git a/tests/js/permission-tests.js b/tests/js/permission-tests.js index d396f725..39ed46bd 100644 --- a/tests/js/permission-tests.js +++ b/tests/js/permission-tests.js @@ -33,7 +33,7 @@ function createUsersWithTestRealms(count) { return Realm.Sync.User .register('http://localhost:9080', uuid(), 'password') .then(user => { - new Realm({sync: {user, url: 'realm://localhost:9080/~/test'}}).close(); + new Realm({sync: {user, url: 'realm://localhost:9080/~/test', fullSynchronization: true }}).close(); return user; }); }; @@ -185,7 +185,7 @@ module.exports = { } } ], - sync: {user: user, url: url, partial: true} + sync: {user: user, url: url, fullSynchronization: false } }; }; let owner, otherUser @@ -193,7 +193,7 @@ module.exports = { .register('http://localhost:9080', uuid(), 'password') .then(user => { owner = user; - new Realm({sync: {user, url: 'realm://localhost:9080/default', partial: true}}).close(); + new Realm({sync: {user, url: 'realm://localhost:9080/default'}}).close(); return Realm.Sync.User.register('http://localhost:9080', uuid(), 'password') }) .then((user) => { diff --git a/tests/js/session-tests.js b/tests/js/session-tests.js index e6f3970a..dc8297c9 100644 --- a/tests/js/session-tests.js +++ b/tests/js/session-tests.js @@ -116,7 +116,7 @@ module.exports = { // 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 config = user.createConfiguration({ sync: { url: 'realm://localhost:9080/~/myrealm', error: postTokenRefreshChecks, fullSynchronization: true } }); const realm = new Realm(config); const session = realm.syncSession; TestCase.assertInstanceOf(session, Realm.Sync.Session); @@ -148,7 +148,7 @@ module.exports = { let successCounter = 0; config = { - sync: { user, url: `realm://localhost:9080/~/${realmName}` }, + sync: { user, url: `realm://localhost:9080/~/${realmName}`, fullSynchronization: true }, schema: [{ name: 'Dog', properties: { name: 'string' } }], }; @@ -184,7 +184,7 @@ module.exports = { let successCounter = 0; config = { - sync: { user, url: `realm://localhost:9080/~/${realmName}` }, + sync: { user, url: `realm://localhost:9080/~/${realmName}`, fullSynchronization: true }, schema: [{ name: 'Dog', properties: { name: 'string' } }], schemaVersion: 1, }; @@ -226,7 +226,7 @@ module.exports = { let successCounter = 0; let config = { - sync: { user, url: `realm://localhost:9080/~/${realmName}` }, + sync: { user, url: `realm://localhost:9080/~/${realmName}`, fullSynchronization: true }, schema: [{ name: 'Dog', properties: { name: 'string' } }], }; return new Promise((resolve, reject) => { @@ -277,7 +277,7 @@ module.exports = { let successCounter = 0; let config = { - sync: { user, url: `realm://localhost:9080/~/${realmName}` } + sync: { user, url: `realm://localhost:9080/~/${realmName}`, fullSynchronization: true } }; return new Promise((resolve, reject) => { Realm.openAsync(config, (error, realm) => { @@ -372,7 +372,7 @@ module.exports = { testErrorHandling() { return Realm.Sync.User.register('http://localhost:9080', uuid(), 'password').then(user => { return new Promise((resolve, _reject) => { - const config = { sync: { user, url: 'realm://localhost:9080/~/myrealm' } }; + const config = user.createConfiguration({ sync: { url: 'realm://localhost:9080/~/myrealm' } }); config.sync.error = (sender, error) => { try { TestCase.assertEqual(error.message, 'simulated error'); @@ -405,7 +405,7 @@ module.exports = { .then(user => { let config = { schema: [schemas.ParentObject, schemas.NameObject], - sync: { user, url: `realm://localhost:9080/~/${realmName}` } + sync: { user, url: `realm://localhost:9080/~/${realmName}`, fullSynchronization: true } }; return Realm.open(config) }).then(realm => { @@ -744,8 +744,8 @@ module.exports = { sync: { user: user, url: `realm://localhost:9080/default/__partial/`, - partial: true, - _disablePartialSyncUrlChecks: true + _disableQueryBasedSyncUrlChecks: true, + fullSynchronization: false, } }; const realm = new Realm(config1); @@ -758,7 +758,7 @@ module.exports = { sync: { user: user, url: `realm://localhost:9080/default/__partial/`, // <--- not allowed URL - partial: true, + fullSynchronization: false, } }; TestCase.assertThrows(() => new Realm(config2)); @@ -769,7 +769,7 @@ module.exports = { sync: { user: user, url: 'realm://localhost:9080/~/default', - partial: false, // <---- calling subscribe should fail + fullSynchronization: true, // <---- calling subscribe should fail error: (session, error) => console.log(error) }, schema: [{ name: 'Dog', properties: { name: 'string' } }] @@ -798,7 +798,7 @@ module.exports = { defaultRealmInvalidArguments(); return new Promise((resolve, reject) => { - let config = Realm.automaticSyncConfiguration(); + let config = Realm.Sync.User.current.createConfiguration(); config.schema = [{ name: 'Dog', properties: { name: 'string' } }]; Realm.deleteFile(config); @@ -874,7 +874,7 @@ module.exports = { return Realm.Sync.User.register('http://localhost:9080', uuid(), 'password').then(user => { return new Promise((resolve, _reject) => { var realm; - const config = { sync: { user, url: 'realm://localhost:9080/~/myrealm' } }; + const config = user.createConfiguration({ sync: { url: 'realm://localhost:9080/~/myrealm' } }); config.sync.error = (sender, error) => { try { TestCase.assertEqual(error.name, 'ClientReset'); diff --git a/tests/js/user-tests.js b/tests/js/user-tests.js index b09fd447..edc9aa37 100644 --- a/tests/js/user-tests.js +++ b/tests/js/user-tests.js @@ -130,7 +130,8 @@ module.exports = { }).then((user => { assertIsUser(user); // Can we open a realm with the logged-in user? - const realm = new Realm({ sync: { user: user, url: 'realm://localhost:9080/~/test' } }); + const config = user.createConfiguration({ sync: { url: 'realm://localhost:9080/~/test' }}); + const realm = new Realm(config); TestCase.assertInstanceOf(realm, Realm); realm.close(); })) @@ -143,7 +144,7 @@ module.exports = { return Realm.Sync.User.authenticate('http://localhost:9080', 'password', { username: username, password: 'password' }); }).then((user => { assertIsUser(user); - const realm = new Realm({ sync: { user: user, url: 'realm://localhost:9080/~/test' } }); + const realm = new Realm(user.createConfiguration({ sync: { url: 'realm://localhost:9080/~/test' } })); TestCase.assertInstanceOf(realm, Realm); realm.close(); })) @@ -318,6 +319,58 @@ module.exports = { }).then(account => { if (account) { throw new Error("Retrieving nonexistent account should fail"); }}); }, + testCreateConfiguration_defaultConfig() { + const username = uuid(); + return Realm.Sync.User.register('http://localhost:9080', username, 'password').then((user) => { + let config = user.createConfiguration(); + TestCase.assertEqual(config.sync.url, "realm://localhost:9080/default"); + TestCase.assertUndefined(config.sync.partial); + TestCase.assertFalse(config.sync.fullSynchronization); + }); + }, + + testCreateConfiguration_useOldConfiguration() { + const username = uuid(); + return Realm.Sync.User.register('http://localhost:9080', username, 'password').then((user) => { + let config = user.createConfiguration({ sync: { url: 'http://localhost:9080/other_realm', partial: true }}); + TestCase.assertEqual(config.sync.url, 'http://localhost:9080/other_realm'); + TestCase.assertUndefined(config.sync.fullSynchronization); + TestCase.assertTrue(config.sync.partial); + }); + }, + + testCreateConfiguration_settingPartialAndFullSynchronizationThrows() { + const username = uuid(); + return Realm.Sync.User.register('http://localhost:9080', username, 'password').then((user) => { + TestCase.assertThrowsContaining(() => { + let config = { + sync: { + url: 'http://localhost:9080/~/default', + partial: true, + fullSynchronization: false + } + }; + user.createConfiguration(config); + }, "'partial' and 'fullSynchronization' were both set. 'partial' has been deprecated, use only 'fullSynchronization'"); + }); + }, + + testOpen_partialAndFullSynchronizationSetThrows() { + const username = uuid(); + return Realm.Sync.User.register('http://localhost:9080', username, 'password').then((user) => { + TestCase.assertThrowsContaining(() => { + new Realm({ + sync: { + user: user, + url: 'http://localhost:9080/~/default', + partial: false, + fullSynchronization: true + } + }) + }, "'partial' and 'fullSynchronization' were both set. 'partial' has been deprecated, use only 'fullSynchronization'"); + }); + } + /* This test fails because of realm-object-store #243 . We should use 2 users. testSynchronizeChangesWithTwoClientsAndOneUser() { // Test Schema