React to schema changes (#1831)

* Adding schema change listener (#1825).
This commit is contained in:
Kenneth Geisshirt 2018-05-29 12:22:34 +02:00 committed by GitHub
parent 16bea5f42b
commit 58671dd59e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 121 additions and 16 deletions

View File

@ -6,7 +6,7 @@ x.x.x Release notes (xxxx-xx-xx)
* None. * None.
### Enhancements ### Enhancements
* None * Added schema change listener to `Realm.addListener()` (#1825).
### Bug fixes ### Bug fixes
* Fix `Realm.open()` to work without passing a config. * Fix `Realm.open()` to work without passing a config.

View File

@ -209,8 +209,8 @@ class Realm {
/** /**
* Add a listener `callback` for the specified event `name`. * Add a listener `callback` for the specified event `name`.
* @param {string} name - The name of event that should cause the callback to be called. * @param {string} name - The name of event that should cause the callback to be called.
* _Currently, only the "change" event supported_. * _Currently, only the "change" and "schema" are events supported_.
* @param {callback(Realm, string)} callback - Function to be called when the event occurs. * @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 * Each callback will only be called once per event, regardless of the number of times
* it was added. * it was added.
* @throws {Error} If an invalid event `name` is supplied, or if `callback` is not a function. * @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`. * Remove the listener `callback` for the specfied event `name`.
* @param {string} name - The event name. * @param {string} name - The event name.
* _Currently, only the "change" event supported_. * _Currently, only the "change" and "schema" are events supported_.
* @param {callback(Realm, string)} callback - Function that was previously added as a * @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. * 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. * @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). * Remove all event listeners (restricted to the event `name`, if provided).
* @param {string} [name] - The name of the event whose listeners should be removed. * @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 * @throws {Error} When invalid event `name` is supplied
*/ */
removeAllListeners(name) {} removeAllListeners(name) {}

2
lib/index.d.ts vendored
View File

@ -669,6 +669,7 @@ declare class Realm {
* @returns void * @returns void
*/ */
addListener(name: string, callback: (sender: Realm, event: 'change') => void): 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 * @param {string} name
@ -676,6 +677,7 @@ declare class Realm {
* @returns void * @returns void
*/ */
removeListener(name: string, callback: (sender: Realm, event: 'change') => void): 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? * @param {string} name?

View File

@ -81,6 +81,10 @@ class RealmDelegate : public BindingContext {
notify("change"); notify("change");
} }
virtual void schema_did_change(realm::Schema const& schema) {
schema_notify("schema", schema);
}
RealmDelegate(std::weak_ptr<realm::Realm> realm, GlobalContextType ctx) : m_context(ctx), m_realm(realm) {} RealmDelegate(std::weak_ptr<realm::Realm> realm, GlobalContextType ctx) : m_context(ctx), m_realm(realm) {}
~RealmDelegate() { ~RealmDelegate() {
@ -88,6 +92,7 @@ class RealmDelegate : public BindingContext {
m_defaults.clear(); m_defaults.clear();
m_constructors.clear(); m_constructors.clear();
m_notifications.clear(); m_notifications.clear();
m_schema_notifications.clear();
} }
void add_notification(FunctionType notification) { void add_notification(FunctionType notification) {
@ -98,6 +103,7 @@ class RealmDelegate : public BindingContext {
} }
m_notifications.emplace_back(m_context, notification); m_notifications.emplace_back(m_context, notification);
} }
void remove_notification(FunctionType notification) { void remove_notification(FunctionType notification) {
for (auto iter = m_notifications.begin(); iter != m_notifications.end(); ++iter) { for (auto iter = m_notifications.begin(); iter != m_notifications.end(); ++iter) {
if (*iter == notification) { if (*iter == notification) {
@ -106,16 +112,42 @@ class RealmDelegate : public BindingContext {
} }
} }
} }
void remove_all_notifications() { void remove_all_notifications() {
m_notifications.clear(); 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; ObjectDefaultsMap m_defaults;
ConstructorMap m_constructors; ConstructorMap m_constructors;
private: private:
Protected<GlobalContextType> m_context; Protected<GlobalContextType> m_context;
std::list<Protected<FunctionType>> m_notifications; std::list<Protected<FunctionType>> m_notifications;
std::list<Protected<FunctionType>> m_schema_notifications;
std::weak_ptr<realm::Realm> m_realm; std::weak_ptr<realm::Realm> m_realm;
void notify(const char *notification_name) { 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<T, RealmClass<T>>(m_context, new SharedRealm(realm));
ObjectType schema_object = Schema<T>::object_for_schema(m_context, schema);
ValueType arguments[] = {realm_object, Value::from_string(m_context, notification_name), schema_object};
std::list<Protected<FunctionType>> notifications_copy(m_schema_notifications);
for (auto &callback : notifications_copy) {
Function<T>::callback(m_context, callback, realm_object, 3, arguments);
}
}
friend class RealmClass<T>; friend class RealmClass<T>;
}; };
@ -288,11 +338,11 @@ public:
static std::string validated_notification_name(ContextType ctx, const ValueType &value) { static std::string validated_notification_name(ContextType ctx, const ValueType &value) {
std::string name = Value::validated_to_string(ctx, value, "notification name"); std::string name = Value::validated_to_string(ctx, value, "notification name");
if (name != "change") { if (name == "change" || name == "schema") {
throw std::runtime_error("Only the 'change' notification name is supported.");
}
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) { static const ObjectSchema& validated_object_schema_for_value(ContextType ctx, const SharedRealm &realm, const ValueType &value) {
std::string object_type; std::string object_type;
@ -954,37 +1004,53 @@ template<typename T>
void RealmClass<T>::add_listener(ContextType ctx, ObjectType this_object, Arguments args, ReturnValue &return_value) { void RealmClass<T>::add_listener(ContextType ctx, ObjectType this_object, Arguments args, ReturnValue &return_value) {
args.validate_maximum(2); 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]); auto callback = Value::validated_to_function(ctx, args[1]);
SharedRealm realm = *get_internal<T, RealmClass<T>>(this_object); SharedRealm realm = *get_internal<T, RealmClass<T>>(this_object);
realm->verify_open(); realm->verify_open();
if (name == "change") {
get_delegate<T>(realm.get())->add_notification(callback); get_delegate<T>(realm.get())->add_notification(callback);
} }
else {
get_delegate<T>(realm.get())->add_schema_notification(callback);
}
}
template<typename T> template<typename T>
void RealmClass<T>::remove_listener(ContextType ctx, ObjectType this_object, Arguments args, ReturnValue &return_value) { void RealmClass<T>::remove_listener(ContextType ctx, ObjectType this_object, Arguments args, ReturnValue &return_value) {
args.validate_maximum(2); 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]); auto callback = Value::validated_to_function(ctx, args[1]);
SharedRealm realm = *get_internal<T, RealmClass<T>>(this_object); SharedRealm realm = *get_internal<T, RealmClass<T>>(this_object);
realm->verify_open(); realm->verify_open();
if (name == "change") {
get_delegate<T>(realm.get())->remove_notification(callback); get_delegate<T>(realm.get())->remove_notification(callback);
} }
else {
get_delegate<T>(realm.get())->remove_schema_notification(callback);
}
}
template<typename T> template<typename T>
void RealmClass<T>::remove_all_listeners(ContextType ctx, ObjectType this_object, Arguments args, ReturnValue &return_value) { void RealmClass<T>::remove_all_listeners(ContextType ctx, ObjectType this_object, Arguments args, ReturnValue &return_value) {
args.validate_maximum(1); args.validate_maximum(1);
std::string name = "change";
if (args.count) { if (args.count) {
validated_notification_name(ctx, args[0]); name = validated_notification_name(ctx, args[0]);
} }
SharedRealm realm = *get_internal<T, RealmClass<T>>(this_object); SharedRealm realm = *get_internal<T, RealmClass<T>>(this_object);
realm->verify_open(); realm->verify_open();
if (name == "change") {
get_delegate<T>(realm.get())->remove_all_notifications(); get_delegate<T>(realm.get())->remove_all_notifications();
} }
else {
get_delegate<T>(realm.get())->remove_all_schema_notifications();
}
}
template<typename T> template<typename T>
void RealmClass<T>::close(ContextType ctx, ObjectType this_object, Arguments args, ReturnValue &return_value) { void RealmClass<T>::close(ContextType ctx, ObjectType this_object, Arguments args, ReturnValue &return_value) {

View File

@ -803,7 +803,7 @@ module.exports = {
TestCase.assertEqual(secondNotificationCount, 1); TestCase.assertEqual(secondNotificationCount, 1);
TestCase.assertThrowsContaining(() => realm.addListener('invalid', () => {}), TestCase.assertThrowsContaining(() => realm.addListener('invalid', () => {}),
"Only the 'change' notification name is supported."); "Only the 'change' and 'schema' notification names are supported.");
realm.addListener('change', () => { realm.addListener('change', () => {
throw new Error('expected error message'); 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.'); }, '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 // FIXME: reanble test
/* /*
testWriteCopyTo: function() { testWriteCopyTo: function() {