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.
### Enhancements
* None
* Added schema change listener to `Realm.addListener()` (#1825).
### Bug fixes
* 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`.
* @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) {}

2
lib/index.d.ts vendored
View File

@ -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?

View File

@ -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::Realm> 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<GlobalContextType> m_context;
std::list<Protected<FunctionType>> m_notifications;
std::list<Protected<FunctionType>> m_schema_notifications;
std::weak_ptr<realm::Realm> 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<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>;
};
@ -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<typename T>
void RealmClass<T>::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<T, RealmClass<T>>(this_object);
realm->verify_open();
get_delegate<T>(realm.get())->add_notification(callback);
if (name == "change") {
get_delegate<T>(realm.get())->add_notification(callback);
}
else {
get_delegate<T>(realm.get())->add_schema_notification(callback);
}
}
template<typename T>
void RealmClass<T>::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<T, RealmClass<T>>(this_object);
realm->verify_open();
get_delegate<T>(realm.get())->remove_notification(callback);
if (name == "change") {
get_delegate<T>(realm.get())->remove_notification(callback);
}
else {
get_delegate<T>(realm.get())->remove_schema_notification(callback);
}
}
template<typename T>
void RealmClass<T>::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<T, RealmClass<T>>(this_object);
realm->verify_open();
get_delegate<T>(realm.get())->remove_all_notifications();
if (name == "change") {
get_delegate<T>(realm.get())->remove_all_notifications();
}
else {
get_delegate<T>(realm.get())->remove_all_schema_notifications();
}
}
template<typename T>

View File

@ -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() {