diff --git a/CHANGELOG.md b/CHANGELOG.md index 13ad0c4f..fa36527d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ x.x.x Release notes (xxxx-xx-xx) * None. ### Enhancements -* None +* Added schema change listener to `Realm.addListener()` (#1825). ### Bug fixes * Fix `Realm.open()` to work without passing a config. diff --git a/docs/realm.js b/docs/realm.js index dc4b001e..067f1d17 100644 --- a/docs/realm.js +++ b/docs/realm.js @@ -209,8 +209,8 @@ class Realm { /** * Add a listener `callback` for the specified event `name`. * @param {string} name - The name of event that should cause the callback to be called. - * _Currently, only the "change" event supported_. - * @param {callback(Realm, string)} callback - Function to be called when the event occurs. + * _Currently, only the "change" and "schema" are events supported_. + * @param {callback(Realm, string)|callback(Realm, string, Schema)} callback - Function to be called when a change event occurs. * Each callback will only be called once per event, regardless of the number of times * it was added. * @throws {Error} If an invalid event `name` is supplied, or if `callback` is not a function. @@ -220,8 +220,8 @@ class Realm { /** * Remove the listener `callback` for the specfied event `name`. * @param {string} name - The event name. - * _Currently, only the "change" event supported_. - * @param {callback(Realm, string)} callback - Function that was previously added as a + * _Currently, only the "change" and "schema" are events supported_. + * @param {callback(Realm, string)|callback(Realm, string, Schema)} callback - Function that was previously added as a * listener for this event through the {@link Realm#addListener addListener} method. * @throws {Error} If an invalid event `name` is supplied, or if `callback` is not a function. */ @@ -230,7 +230,7 @@ class Realm { /** * Remove all event listeners (restricted to the event `name`, if provided). * @param {string} [name] - The name of the event whose listeners should be removed. - * _Currently, only the "change" event supported_. + * _Currently, only the "change" and "schema" are events supported_. * @throws {Error} When invalid event `name` is supplied */ removeAllListeners(name) {} diff --git a/lib/index.d.ts b/lib/index.d.ts index 05997e81..d0843bf4 100644 --- a/lib/index.d.ts +++ b/lib/index.d.ts @@ -669,6 +669,7 @@ declare class Realm { * @returns void */ addListener(name: string, callback: (sender: Realm, event: 'change') => void): void; + addListener(name: string, callback: (sender: Realm, event: 'schema', schema: Realm.ObjectSchema[]) => void): void; /** * @param {string} name @@ -676,6 +677,7 @@ declare class Realm { * @returns void */ removeListener(name: string, callback: (sender: Realm, event: 'change') => void): void; + removeListener(name: string, callback: (sender: Realm, event: 'schema', schema: Realm.ObjectSchema[]) => void): void; /** * @param {string} name? diff --git a/src/js_realm.hpp b/src/js_realm.hpp index d38f783c..9e1f58aa 100644 --- a/src/js_realm.hpp +++ b/src/js_realm.hpp @@ -81,6 +81,10 @@ class RealmDelegate : public BindingContext { notify("change"); } + virtual void schema_did_change(realm::Schema const& schema) { + schema_notify("schema", schema); + } + RealmDelegate(std::weak_ptr realm, GlobalContextType ctx) : m_context(ctx), m_realm(realm) {} ~RealmDelegate() { @@ -88,6 +92,7 @@ class RealmDelegate : public BindingContext { m_defaults.clear(); m_constructors.clear(); m_notifications.clear(); + m_schema_notifications.clear(); } void add_notification(FunctionType notification) { @@ -98,6 +103,7 @@ class RealmDelegate : public BindingContext { } m_notifications.emplace_back(m_context, notification); } + void remove_notification(FunctionType notification) { for (auto iter = m_notifications.begin(); iter != m_notifications.end(); ++iter) { if (*iter == notification) { @@ -106,16 +112,42 @@ class RealmDelegate : public BindingContext { } } } + void remove_all_notifications() { m_notifications.clear(); } + void add_schema_notification(FunctionType notification) { + SharedRealm realm = m_realm.lock(); + realm->read_group(); // to get the schema change handler going + for (auto &handler : m_schema_notifications) { + if (handler == notification) { + return; + } + } + m_schema_notifications.emplace_back(m_context, notification); + } + + void remove_schema_notification(FunctionType notification) { + for (auto iter = m_schema_notifications.begin(); iter != m_schema_notifications.end(); ++iter) { + if (*iter == notification) { + m_schema_notifications.erase(iter); + return; + } + } + } + + void remove_all_schema_notifications() { + m_schema_notifications.clear(); + } + ObjectDefaultsMap m_defaults; ConstructorMap m_constructors; private: Protected m_context; std::list> m_notifications; + std::list> m_schema_notifications; std::weak_ptr m_realm; void notify(const char *notification_name) { @@ -135,6 +167,24 @@ class RealmDelegate : public BindingContext { } } + void schema_notify(const char *notification_name, realm::Schema const& schema) { + HANDLESCOPE + + SharedRealm realm = m_realm.lock(); + if (!realm) { + throw std::runtime_error("Realm no longer exists"); + } + + ObjectType realm_object = create_object>(m_context, new SharedRealm(realm)); + ObjectType schema_object = Schema::object_for_schema(m_context, schema); + ValueType arguments[] = {realm_object, Value::from_string(m_context, notification_name), schema_object}; + + std::list> notifications_copy(m_schema_notifications); + for (auto &callback : notifications_copy) { + Function::callback(m_context, callback, realm_object, 3, arguments); + } + } + friend class RealmClass; }; @@ -288,10 +338,10 @@ public: static std::string validated_notification_name(ContextType ctx, const ValueType &value) { std::string name = Value::validated_to_string(ctx, value, "notification name"); - if (name != "change") { - throw std::runtime_error("Only the 'change' notification name is supported."); + if (name == "change" || name == "schema") { + return name; } - return name; + throw std::runtime_error("Only the 'change' and 'schema' notification names are supported."); } static const ObjectSchema& validated_object_schema_for_value(ContextType ctx, const SharedRealm &realm, const ValueType &value) { @@ -954,36 +1004,52 @@ template void RealmClass::add_listener(ContextType ctx, ObjectType this_object, Arguments args, ReturnValue &return_value) { args.validate_maximum(2); - validated_notification_name(ctx, args[0]); + auto name = validated_notification_name(ctx, args[0]); auto callback = Value::validated_to_function(ctx, args[1]); SharedRealm realm = *get_internal>(this_object); realm->verify_open(); - get_delegate(realm.get())->add_notification(callback); + if (name == "change") { + get_delegate(realm.get())->add_notification(callback); + } + else { + get_delegate(realm.get())->add_schema_notification(callback); + } } template void RealmClass::remove_listener(ContextType ctx, ObjectType this_object, Arguments args, ReturnValue &return_value) { args.validate_maximum(2); - validated_notification_name(ctx, args[0]); + auto name = validated_notification_name(ctx, args[0]); auto callback = Value::validated_to_function(ctx, args[1]); SharedRealm realm = *get_internal>(this_object); realm->verify_open(); - get_delegate(realm.get())->remove_notification(callback); + if (name == "change") { + get_delegate(realm.get())->remove_notification(callback); + } + else { + get_delegate(realm.get())->remove_schema_notification(callback); + } } template void RealmClass::remove_all_listeners(ContextType ctx, ObjectType this_object, Arguments args, ReturnValue &return_value) { args.validate_maximum(1); + std::string name = "change"; if (args.count) { - validated_notification_name(ctx, args[0]); + name = validated_notification_name(ctx, args[0]); } SharedRealm realm = *get_internal>(this_object); realm->verify_open(); - get_delegate(realm.get())->remove_all_notifications(); + if (name == "change") { + get_delegate(realm.get())->remove_all_notifications(); + } + else { + get_delegate(realm.get())->remove_all_schema_notifications(); + } } template diff --git a/tests/js/realm-tests.js b/tests/js/realm-tests.js index 777ede1d..d85f699c 100644 --- a/tests/js/realm-tests.js +++ b/tests/js/realm-tests.js @@ -803,7 +803,7 @@ module.exports = { TestCase.assertEqual(secondNotificationCount, 1); TestCase.assertThrowsContaining(() => realm.addListener('invalid', () => {}), - "Only the 'change' notification name is supported."); + "Only the 'change' and 'schema' notification names are supported."); realm.addListener('change', () => { throw new Error('expected error message'); @@ -1220,6 +1220,43 @@ module.exports = { }, 'The Realm file format must be allowed to be upgraded in order to proceed.'); }, + + // FIXME: We need to test adding a property also calls the listener + testSchemaUpdatesNewClass: function() { + return new Promise((resolve, reject) => { + let realm1 = new Realm({ _cache: false }); + TestCase.assertTrue(realm1.empty); + TestCase.assertEqual(realm1.schema.length, 0); // empty schema + realm1.addListener('schema', (realm, event, schema) => { + TestCase.assertEqual(event, 'schema'); + TestCase.assertEqual(schema.length, 1); + TestCase.assertEqual(realm.schema.length, 1); + TestCase.assertEqual(schema[0].name, 'TestObject'); + TestCase.assertEqual(realm1.schema.length, 1); + TestCase.assertEqual(realm.schema[0].name, 'TestObject'); + }); + + const schema = [{ + name: 'TestObject', + properties: { + prop0: 'string', + } + }]; + + let realm2 = new Realm({ schema: schema, _cache: false }); + TestCase.assertEqual(realm1.schema.length, 0); // not yet updated + TestCase.assertEqual(realm2.schema.length, 1); + + // give some time to let advance_read to complete + // in real world, a Realm will not be closed just after its + // schema has been updated + setTimeout(() => { + resolve(); + }, 1000); + }); + }, + + // FIXME: reanble test /* testWriteCopyTo: function() {