diff --git a/examples/ReactExample/components/realm.js b/examples/ReactExample/components/realm.js index 13d042ad..6e71639a 100644 --- a/examples/ReactExample/components/realm.js +++ b/examples/ReactExample/components/realm.js @@ -34,6 +34,7 @@ TodoList.schema = { name: 'TodoList', properties: { name: 'string', + creationDate: 'date', items: {type: 'list', objectType: 'Todo'}, }, }; diff --git a/examples/ReactExample/components/todo-app.js b/examples/ReactExample/components/todo-app.js index c0d03f70..d2ff4fe4 100644 --- a/examples/ReactExample/components/todo-app.js +++ b/examples/ReactExample/components/todo-app.js @@ -38,15 +38,18 @@ export default class TodoApp extends React.Component { constructor(props) { super(props); - let todoLists = realm.objects('TodoList'); - if (todoLists.length < 1) { + // This is a Results object, which will live-update. + this.todoLists = realm.objects('TodoList').sorted('creationDate'); + if (this.todoLists.length < 1) { realm.write(() => { - realm.create('TodoList', {name: 'Todo List'}); + realm.create('TodoList', {name: 'Todo List', creationDate: new Date()}); }); } + this.todoLists.addListener((name, changes) => { + console.log("changed: " + JSON.stringify(changes)); + }); + console.log("registered listener"); - // This is a Results object, which will live-update. - this.todoLists = todoLists; // Bind all the methods that we will be passing as props. this.renderScene = this.renderScene.bind(this); @@ -79,7 +82,6 @@ export default class TodoApp extends React.Component { component: TodoListView, passProps: { ref: 'listView', - items: this.todoLists, extraItems: extraItems, onPressItem: this._onPressTodoList, }, @@ -105,7 +107,8 @@ export default class TodoApp extends React.Component { } renderScene(route) { - return + console.log(this.todoLists); + return } _addNewTodoItem(list) { @@ -128,7 +131,7 @@ export default class TodoApp extends React.Component { } realm.write(() => { - realm.create('TodoList', {name: ''}); + realm.create('TodoList', {name: '', creationDate: new Date()}); }); this._setEditingRow(items.length - 1); diff --git a/src/RealmJS.xcodeproj/project.pbxproj b/src/RealmJS.xcodeproj/project.pbxproj index 7613a1dc..52761cd8 100644 --- a/src/RealmJS.xcodeproj/project.pbxproj +++ b/src/RealmJS.xcodeproj/project.pbxproj @@ -123,6 +123,7 @@ 029048101C0428DF00ABDED4 /* rpc.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = rpc.hpp; sourceTree = ""; }; 029048351C042A3C00ABDED4 /* platform.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = platform.hpp; sourceTree = ""; }; 029048381C042A8F00ABDED4 /* platform.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = platform.mm; sourceTree = ""; }; + 0290934A1CEFA9170009769E /* js_observable.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = js_observable.hpp; sourceTree = ""; }; 02A3C7A41BC4341500B1A7BE /* libc++.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = "libc++.tbd"; path = "usr/lib/libc++.tbd"; sourceTree = SDKROOT; }; 02B58CBC1AE99CEC009B348C /* RealmJSTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RealmJSTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 02B58CCD1AE99D4D009B348C /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; }; @@ -288,6 +289,7 @@ F6874A441CAD2ACD00EEEE36 /* JSC */, F62BF9001CAC72C40022BCDC /* Node */, F62A35141C18E783004A917D /* Object Store */, + 0290934A1CEFA9170009769E /* js_observable.hpp */, F60102F71CBDA6D400EC01BA /* js_collection.hpp */, 029048041C0428DF00ABDED4 /* js_list.hpp */, 029048061C0428DF00ABDED4 /* js_realm_object.hpp */, diff --git a/src/js_collection.hpp b/src/js_collection.hpp index 510333a7..9a758d18 100644 --- a/src/js_collection.hpp +++ b/src/js_collection.hpp @@ -19,6 +19,10 @@ #pragma once #include "js_class.hpp" +#include "js_types.hpp" +#include "js_observable.hpp" + +#include "collection_notifications.hpp" namespace realm { namespace js { @@ -27,9 +31,40 @@ namespace js { class Collection {}; template -struct CollectionClass : ClassDefinition { +struct CollectionClass : ClassDefinition> { + using ContextType = typename T::Context; + using ValueType = typename T::Value; + using ObjectType = typename T::Object; + using Object = js::Object; + using Value = js::Value; + std::string const name = "Collection"; + + static inline ValueType create_collection_change_set(ContextType ctx, const CollectionChangeSet &change_set); }; +template +typename T::Value CollectionClass::create_collection_change_set(ContextType ctx, const CollectionChangeSet &change_set) +{ + ObjectType object = Object::create_empty(ctx); + std::vector deletions, insertions, modifications; + for (auto index : change_set.deletions.as_indexes()) { + deletions.push_back(Value::from_number(ctx, index)); + } + Object::set_property(ctx, object, "deletions", Object::create_array(ctx, deletions)); + + for (auto index : change_set.insertions.as_indexes()) { + insertions.push_back(Value::from_number(ctx, index)); + } + Object::set_property(ctx, object, "insertions", Object::create_array(ctx, insertions)); + + for (auto index : change_set.modifications.as_indexes()) { + modifications.push_back(Value::from_number(ctx, index)); + } + Object::set_property(ctx, object, "modifications", Object::create_array(ctx, modifications)); + + return object; +} + } // js } // realm diff --git a/src/js_list.hpp b/src/js_list.hpp index 35c632e1..e8d40432 100644 --- a/src/js_list.hpp +++ b/src/js_list.hpp @@ -33,10 +33,20 @@ namespace realm { namespace js { template -struct ListClass : ClassDefinition> { +class List : public realm::List { + public: + List(std::shared_ptr r, const ObjectSchema& s, LinkViewRef l) noexcept : realm::List(r, l) {} + List(const realm::List &l) : realm::List(l) {} + + std::map, NotificationToken, typename Protected::Comparator> m_notification_tokens; +}; + +template +struct ListClass : ClassDefinition, CollectionClass> { using ContextType = typename T::Context; using ObjectType = typename T::Object; using ValueType = typename T::Value; + using FunctionType = typename T::Function; using Object = js::Object; using Value = js::Value; using ReturnValue = js::ReturnValue; @@ -58,7 +68,12 @@ struct ListClass : ClassDefinition> { static void filtered(ContextType, ObjectType, size_t, const ValueType[], ReturnValue &); static void sorted(ContextType, ObjectType, size_t, const ValueType[], ReturnValue &); static void is_valid(ContextType, ObjectType, size_t, const ValueType [], ReturnValue &); - + + // observable + static void add_listener(ContextType, ObjectType, size_t, const ValueType[], ReturnValue &); + static void remove_listener(ContextType, ObjectType, size_t, const ValueType[], ReturnValue &); + static void remove_all_listeners(ContextType, ObjectType, size_t, const ValueType[], ReturnValue &); + std::string const name = "List"; MethodMap const methods = { @@ -71,6 +86,9 @@ struct ListClass : ClassDefinition> { {"filtered", wrap}, {"sorted", wrap}, {"isValid", wrap}, + {"addListener", wrap}, + {"removeListener", wrap}, + {"removeAllListeners", wrap}, }; PropertyMap const properties = { @@ -82,7 +100,7 @@ struct ListClass : ClassDefinition> { template typename T::Object ListClass::create_instance(ContextType ctx, realm::List list) { - return create_object>(ctx, new realm::List(std::move(list))); + return create_object>(ctx, new realm::js::List(std::move(list))); } template @@ -230,6 +248,41 @@ template void ListClass::is_valid(ContextType ctx, ObjectType this_object, size_t argc, const ValueType arguments[], ReturnValue &return_value) { return_value.set(get_internal>(this_object)->is_valid()); } + +template +void ListClass::add_listener(ContextType ctx, ObjectType this_object, size_t argc, const ValueType arguments[], ReturnValue &return_value) { + validate_argument_count(argc, 1); + + auto list = get_internal>(this_object); + auto callback = Value::validated_to_function(ctx, arguments[0]); + Protected protected_callback(ctx, callback); + Protected protected_this(ctx, this_object); + Protected protected_ctx(Context::get_global_context(ctx)); + + list->add_notification_callback([=](CollectionChangeSet change_set, std::exception_ptr exception) { + ValueType arguments[2]; + arguments[0] = static_cast(protected_this); + arguments[1] = Value::from_undefined(protected_ctx); + Function::call(protected_ctx, protected_callback, protected_this, 2, arguments); + }); +} + +template +void ListClass::remove_listener(ContextType ctx, ObjectType this_object, size_t argc, const ValueType arguments[], ReturnValue &return_value) { + validate_argument_count(argc, 1); + + auto list = get_internal>(this_object); + auto callback = Value::validated_to_function(ctx, arguments[0]); + list->m_notification_tokens.erase(Protected(ctx, callback)); +} + +template +void ListClass::remove_all_listeners(ContextType ctx, ObjectType this_object, size_t argc, const ValueType arguments[], ReturnValue &return_value) { + validate_argument_count(argc, 0); + + auto list = get_internal>(this_object); + list->m_notification_tokens.clear(); +} } // js } // realm diff --git a/src/js_observable.hpp b/src/js_observable.hpp new file mode 100644 index 00000000..a49a1d0e --- /dev/null +++ b/src/js_observable.hpp @@ -0,0 +1,35 @@ +//////////////////////////////////////////////////////////////////////////// +// +// Copyright 2016 Realm Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +//////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include "js_class.hpp" + +namespace realm { +namespace js { + +// Empty class that merely serves as useful type for now. +class Observable {}; + +template +struct ObservableClass : ClassDefinition { + std::string const name = "Observable"; +}; + +} // js +} // realm diff --git a/src/js_realm.hpp b/src/js_realm.hpp index 06821ac3..a5098c7f 100644 --- a/src/js_realm.hpp +++ b/src/js_realm.hpp @@ -28,6 +28,7 @@ #include "js_list.hpp" #include "js_results.hpp" #include "js_schema.hpp" +#include "js_observable.hpp" #include "shared_realm.hpp" #include "binding_context.hpp" @@ -124,7 +125,7 @@ void set_default_path(std::string path); void delete_all_realms(); template -class RealmClass : public ClassDefinition { +class RealmClass : public ClassDefinition> { using GlobalContextType = typename T::GlobalContext; using ContextType = typename T::Context; using FunctionType = typename T::Function; diff --git a/src/js_results.hpp b/src/js_results.hpp index 6f1ef8e9..bc909f2e 100644 --- a/src/js_results.hpp +++ b/src/js_results.hpp @@ -30,10 +30,29 @@ namespace realm { namespace js { template -struct ResultsClass : ClassDefinition> { +class Results : public realm::Results { + public: + Results(Results const& r) : realm::Results(r) {}; + Results(realm::Results const& r) : realm::Results(r) {}; + Results(Results&&) = default; + Results& operator=(Results&&) = default; + Results& operator=(Results const&) = default; + + Results() = default; + Results(SharedRealm r, Table& table) : realm::Results(r, table) {} + Results(SharedRealm r, Query q, SortOrder s = {}) : realm::Results(r, q, s) {} + Results(SharedRealm r, TableView tv, SortOrder s) : realm::Results(r, tv, s) {} + Results(SharedRealm r, LinkViewRef lv, util::Optional q = {}, SortOrder s = {}) : realm::Results(r, lv, q, s) {} + + std::map, NotificationToken, typename Protected::Comparator> m_notification_tokens; +}; + +template +struct ResultsClass : ClassDefinition, CollectionClass> { using ContextType = typename T::Context; using ObjectType = typename T::Object; using ValueType = typename T::Value; + using FunctionType = typename T::Function; using Object = js::Object; using Value = js::Value; using ReturnValue = js::ReturnValue; @@ -55,6 +74,11 @@ struct ResultsClass : ClassDefinition> { static void sorted(ContextType, ObjectType, size_t, const ValueType[], ReturnValue &); static void is_valid(ContextType, ObjectType, size_t, const ValueType [], ReturnValue &); + // observable + static void add_listener(ContextType, ObjectType, size_t, const ValueType[], ReturnValue &); + static void remove_listener(ContextType, ObjectType, size_t, const ValueType[], ReturnValue &); + static void remove_all_listeners(ContextType, ObjectType, size_t, const ValueType[], ReturnValue &); + std::string const name = "Results"; MethodMap const methods = { @@ -62,6 +86,9 @@ struct ResultsClass : ClassDefinition> { {"filtered", wrap}, {"sorted", wrap}, {"isValid", wrap}, + {"addListener", wrap}, + {"removeListener", wrap}, + {"removeAllListeners", wrap}, }; PropertyMap const properties = { @@ -73,13 +100,13 @@ struct ResultsClass : ClassDefinition> { template typename T::Object ResultsClass::create_instance(ContextType ctx, realm::Results results) { - return create_object>(ctx, new realm::Results(std::move(results))); + return create_object>(ctx, new realm::js::Results(std::move(results))); } template typename T::Object ResultsClass::create_instance(ContextType ctx, SharedRealm realm, const ObjectSchema &object_schema) { auto table = ObjectStore::table_for_object_type(realm->read_group(), object_schema.name); - return create_object>(ctx, new realm::Results(realm, *table)); + return create_object>(ctx, new realm::js::Results(realm, *table)); } template @@ -153,7 +180,7 @@ typename T::Object ResultsClass::create_sorted(ContextType ctx, const U &coll columns.push_back(prop->table_column); } - auto results = new realm::Results(realm, collection.get_query(), {std::move(columns), std::move(ascending)}); + auto results = new realm::js::Results(realm, collection.get_query(), {std::move(columns), std::move(ascending)}); return create_object>(ctx, results); } @@ -206,6 +233,42 @@ template void ResultsClass::is_valid(ContextType ctx, ObjectType this_object, size_t argc, const ValueType arguments[], ReturnValue &return_value) { return_value.set(get_internal>(this_object)->is_valid()); } + +template +void ResultsClass::add_listener(ContextType ctx, ObjectType this_object, size_t argc, const ValueType arguments[], ReturnValue &return_value) { + validate_argument_count(argc, 1); + + auto results = get_internal>(this_object); + auto callback = Value::validated_to_function(ctx, arguments[0]); + Protected protected_callback(ctx, callback); + Protected protected_this(ctx, this_object); + Protected protected_ctx(Context::get_global_context(ctx)); + + auto token = results->add_notification_callback([=](CollectionChangeSet change_set, std::exception_ptr exception) { + ValueType arguments[2]; + arguments[0] = static_cast(protected_this); + arguments[1] = CollectionClass::create_collection_change_set(protected_ctx, change_set); + Function::call(protected_ctx, protected_callback, protected_this, 2, arguments); + }); + results->m_notification_tokens.emplace(protected_callback, std::move(token)); +} +template +void ResultsClass::remove_listener(ContextType ctx, ObjectType this_object, size_t argc, const ValueType arguments[], ReturnValue &return_value) { + validate_argument_count(argc, 1); + + auto results = get_internal>(this_object); + auto callback = Value::validated_to_function(ctx, arguments[0]); + results->m_notification_tokens.erase(Protected(ctx, callback)); +} + +template +void ResultsClass::remove_all_listeners(ContextType ctx, ObjectType this_object, size_t argc, const ValueType arguments[], ReturnValue &return_value) { + validate_argument_count(argc, 0); + + auto results = get_internal>(this_object); + results->m_notification_tokens.clear(); +} + } // js } // realm diff --git a/src/js_types.hpp b/src/js_types.hpp index 2b1f1fbc..d0f3b6a4 100644 --- a/src/js_types.hpp +++ b/src/js_types.hpp @@ -247,6 +247,10 @@ class Protected { bool operator!=(const ValueType &) const; bool operator==(const Protected &) const; bool operator!=(const Protected &) const; + + struct Comparator { + bool operator()(const Protected& a, const Protected& b) const; + }; }; template diff --git a/src/jsc/jsc_protected.hpp b/src/jsc/jsc_protected.hpp index 6c20d039..34ac870f 100644 --- a/src/jsc/jsc_protected.hpp +++ b/src/jsc/jsc_protected.hpp @@ -47,6 +47,12 @@ class Protected { operator bool() const { return m_context != nullptr; } + + struct Comparator { + bool operator() (const Protected& a, const Protected& b) const { + return a.m_context == b.m_context; + } + }; }; template<> @@ -75,6 +81,15 @@ class Protected { operator bool() const { return m_value != nullptr; } + + struct Comparator { + bool operator() (const Protected& a, const Protected& b) const { + if (a.m_context != b.m_context) { + return false; + } + return JSValueIsStrictEqual(a.m_context, a.m_value, b.m_value); + } + }; }; template<> diff --git a/src/node/node_protected.hpp b/src/node/node_protected.hpp index 042a10a1..7253816c 100644 --- a/src/node/node_protected.hpp +++ b/src/node/node_protected.hpp @@ -50,6 +50,10 @@ class Protected { bool operator!=(const Protected &other) const { return m_value != other.m_value; } + + struct Comparator { + bool operator()(const Protected& a, const Protected& b) const { return a == b; } + }; }; } // node diff --git a/tests/ios/RJSModuleLoader.h b/tests/ios/RJSModuleLoader.h index 91ab5bac..a9c8f77c 100644 --- a/tests/ios/RJSModuleLoader.h +++ b/tests/ios/RJSModuleLoader.h @@ -27,5 +27,6 @@ - (JSValue *)loadModuleFromURL:(NSURL *)url error:(NSError **)error; - (JSValue *)loadJSONFromURL:(NSURL *)url error:(NSError **)error; +- (JSValue *)loadGlobalModule:(NSString *)name relativeToURL:(NSURL *)url error:(NSError **)error; @end diff --git a/tests/ios/RJSModuleLoader.m b/tests/ios/RJSModuleLoader.m index 3c0613a9..39cbb066 100644 --- a/tests/ios/RJSModuleLoader.m +++ b/tests/ios/RJSModuleLoader.m @@ -195,6 +195,18 @@ static NSString * const RJSModuleLoaderErrorDomain = @"RJSModuleLoaderErrorDomai BOOL isDirectory; if ([fileManager fileExistsAtPath:moduleURL.path isDirectory:&isDirectory] && isDirectory) { + NSURL *packageURL = [moduleURL URLByAppendingPathComponent:@"package.json"]; + NSDictionary *package; + + if ([fileManager fileExistsAtPath:packageURL.path]) { + NSError *error; + NSData *data = [NSData dataWithContentsOfURL:packageURL options:0 error:&error]; + + package = data ? [NSJSONSerialization JSONObjectWithData:data options:0 error:&error] : nil; + NSAssert(package, @"%@", error); + } + + moduleURL = [moduleURL URLByAppendingPathComponent:package[@"main"] ?: @"index.js"]; return [self loadModuleFromURL:moduleURL error:error]; } diff --git a/tests/ios/RealmJSCoreTests.m b/tests/ios/RealmJSCoreTests.m index d3bf62c9..5fd0cf91 100644 --- a/tests/ios/RealmJSCoreTests.m +++ b/tests/ios/RealmJSCoreTests.m @@ -31,12 +31,23 @@ + (XCTestSuite *)defaultTestSuite { XCTestSuite *suite = [super defaultTestSuite]; - JSContext *context = [[JSContext alloc] init]; + + // We need a JS context from a UIWebView so it has setTimeout, Promise, etc. + UIWebView *webView = [[UIWebView alloc] init]; + JSContext *context = [webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"]; RJSModuleLoader *moduleLoader = [[RJSModuleLoader alloc] initWithContext:context]; NSURL *realmURL = [[NSBundle bundleForClass:self] URLForResource:@"index" withExtension:@"js" subdirectory:@"lib"]; NSURL *scriptURL = [[NSBundle bundleForClass:self] URLForResource:@"index" withExtension:@"js" subdirectory:@"js"]; NSError *error; + // The ES6 global Promise constructor was added in iOS 8. + if (![context[@"Promise"] isObject]) { + JSValue *promiseModule = [moduleLoader loadGlobalModule:@"es6-promise" relativeToURL:scriptURL error:&error]; + NSAssert(promiseModule, @"%@", error); + + context[@"Promise"] = promiseModule[@"Promise"]; + } + // Create Realm constructor in the JS context. RJSInitializeInContext(context.JSGlobalContextRef); @@ -73,25 +84,46 @@ JSContext *context = testObject.context; context.exception = nil; - [testObject invokeMethod:@"runTest" withArguments:@[NSStringFromClass(self.class), method]]; + JSValue *promise = [testObject invokeMethod:@"runTest" withArguments:@[NSStringFromClass(self.class), method]]; - JSValue *exception = context.exception; - if (exception) { - JSValue *message = [exception hasProperty:@"message"] ? exception[@"message"] : exception; - NSString *source = [exception hasProperty:@"sourceURL"] ? [exception[@"sourceURL"] toString] : nil; - NSUInteger line = [exception hasProperty:@"line"] ? [exception[@"line"] toUInt32] - 1 : 0; - NSURL *sourceURL = nil; + if (context.exception) { + [self recordException:context.exception]; + return; + } - if (source) { - NSString *path = [NSString pathWithComponents:@[[@(__FILE__) stringByDeletingLastPathComponent], @"..", @"js", source.lastPathComponent]]; - sourceURL = [NSURL URLWithString:path]; - } + if ([promise isObject]) { + XCTestExpectation *expectation = [self expectationWithDescription:@"Promise resolved or rejected"]; - [self recordFailureWithDescription:message.description - inFile:sourceURL ? sourceURL.absoluteString : @(__FILE__) - atLine:sourceURL ? line : __LINE__ - expected:YES]; + JSValue *onFulfilled = [JSValue valueWithObject:^() { + [expectation fulfill]; + } inContext:context]; + + JSValue *onRejected = [JSValue valueWithObject:^(JSValue *error) { + [self recordException:error]; + [expectation fulfill]; + } inContext:context]; + + [promise invokeMethod:@"then" withArguments:@[onFulfilled, onRejected]]; + + [self waitForExpectationsWithTimeout:5.0 handler:NULL]; } } +- (void)recordException:(JSValue *)exception { + JSValue *message = [exception hasProperty:@"message"] ? exception[@"message"] : exception; + NSString *source = [exception hasProperty:@"sourceURL"] ? [exception[@"sourceURL"] toString] : nil; + NSUInteger line = [exception hasProperty:@"line"] ? [exception[@"line"] toUInt32] - 1 : 0; + NSURL *sourceURL = nil; + + if (source) { + NSString *path = [NSString pathWithComponents:@[[@(__FILE__) stringByDeletingLastPathComponent], @"..", @"js", source.lastPathComponent]]; + sourceURL = [NSURL URLWithString:path]; + } + + [self recordFailureWithDescription:message.description + inFile:sourceURL ? sourceURL.absoluteString : @(__FILE__) + atLine:sourceURL ? line : __LINE__ + expected:YES]; +} + @end diff --git a/tests/js/package.json b/tests/js/package.json index 07bbe26a..c7afa5d5 100644 --- a/tests/js/package.json +++ b/tests/js/package.json @@ -1,5 +1,8 @@ { "name": "realm-tests", "version": "0.0.1", - "private": true + "private": true, + "dependencies": { + "es6-promise": "^3.2.1" + } }