move collection notification changes on top of master

This commit is contained in:
Ari Lazier 2016-08-02 12:42:35 -07:00
commit 49aa5bc0f5
15 changed files with 298 additions and 34 deletions

View File

@ -34,6 +34,7 @@ TodoList.schema = {
name: 'TodoList',
properties: {
name: 'string',
creationDate: 'date',
items: {type: 'list', objectType: 'Todo'},
},
};

View File

@ -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 <route.component {...route.passProps} />
console.log(this.todoLists);
return <route.component items={this.todoLists} {...route.passProps} />
}
_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);

View File

@ -123,6 +123,7 @@
029048101C0428DF00ABDED4 /* rpc.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = rpc.hpp; sourceTree = "<group>"; };
029048351C042A3C00ABDED4 /* platform.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = platform.hpp; sourceTree = "<group>"; };
029048381C042A8F00ABDED4 /* platform.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = platform.mm; sourceTree = "<group>"; };
0290934A1CEFA9170009769E /* js_observable.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = js_observable.hpp; sourceTree = "<group>"; };
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 */,

View File

@ -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<typename T>
struct CollectionClass : ClassDefinition<T, Collection> {
struct CollectionClass : ClassDefinition<T, Collection, ObservableClass<T>> {
using ContextType = typename T::Context;
using ValueType = typename T::Value;
using ObjectType = typename T::Object;
using Object = js::Object<T>;
using Value = js::Value<T>;
std::string const name = "Collection";
static inline ValueType create_collection_change_set(ContextType ctx, const CollectionChangeSet &change_set);
};
template<typename T>
typename T::Value CollectionClass<T>::create_collection_change_set(ContextType ctx, const CollectionChangeSet &change_set)
{
ObjectType object = Object::create_empty(ctx);
std::vector<ValueType> 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

View File

@ -33,10 +33,20 @@ namespace realm {
namespace js {
template<typename T>
struct ListClass : ClassDefinition<T, realm::List, CollectionClass<T>> {
class List : public realm::List {
public:
List(std::shared_ptr<Realm> r, const ObjectSchema& s, LinkViewRef l) noexcept : realm::List(r, l) {}
List(const realm::List &l) : realm::List(l) {}
std::map<Protected<typename T::Function>, NotificationToken, typename Protected<typename T::Function>::Comparator> m_notification_tokens;
};
template<typename T>
struct ListClass : ClassDefinition<T, realm::js::List<T>, CollectionClass<T>> {
using ContextType = typename T::Context;
using ObjectType = typename T::Object;
using ValueType = typename T::Value;
using FunctionType = typename T::Function;
using Object = js::Object<T>;
using Value = js::Value<T>;
using ReturnValue = js::ReturnValue<T>;
@ -58,7 +68,12 @@ struct ListClass : ClassDefinition<T, realm::List, CollectionClass<T>> {
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<T> const methods = {
@ -71,6 +86,9 @@ struct ListClass : ClassDefinition<T, realm::List, CollectionClass<T>> {
{"filtered", wrap<filtered>},
{"sorted", wrap<sorted>},
{"isValid", wrap<is_valid>},
{"addListener", wrap<add_listener>},
{"removeListener", wrap<remove_listener>},
{"removeAllListeners", wrap<remove_all_listeners>},
};
PropertyMap<T> const properties = {
@ -82,7 +100,7 @@ struct ListClass : ClassDefinition<T, realm::List, CollectionClass<T>> {
template<typename T>
typename T::Object ListClass<T>::create_instance(ContextType ctx, realm::List list) {
return create_object<T, ListClass<T>>(ctx, new realm::List(std::move(list)));
return create_object<T, ListClass<T>>(ctx, new realm::js::List<T>(std::move(list)));
}
template<typename T>
@ -230,6 +248,41 @@ template<typename T>
void ListClass<T>::is_valid(ContextType ctx, ObjectType this_object, size_t argc, const ValueType arguments[], ReturnValue &return_value) {
return_value.set(get_internal<T, ListClass<T>>(this_object)->is_valid());
}
template<typename T>
void ListClass<T>::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<T, ListClass<T>>(this_object);
auto callback = Value::validated_to_function(ctx, arguments[0]);
Protected<FunctionType> protected_callback(ctx, callback);
Protected<ObjectType> protected_this(ctx, this_object);
Protected<typename T::GlobalContext> protected_ctx(Context<T>::get_global_context(ctx));
list->add_notification_callback([=](CollectionChangeSet change_set, std::exception_ptr exception) {
ValueType arguments[2];
arguments[0] = static_cast<ObjectType>(protected_this);
arguments[1] = Value::from_undefined(protected_ctx);
Function<T>::call(protected_ctx, protected_callback, protected_this, 2, arguments);
});
}
template<typename T>
void ListClass<T>::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<T, ListClass<T>>(this_object);
auto callback = Value::validated_to_function(ctx, arguments[0]);
list->m_notification_tokens.erase(Protected<FunctionType>(ctx, callback));
}
template<typename T>
void ListClass<T>::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<T, ListClass<T>>(this_object);
list->m_notification_tokens.clear();
}
} // js
} // realm

35
src/js_observable.hpp Normal file
View File

@ -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<typename T>
struct ObservableClass : ClassDefinition<T, Observable> {
std::string const name = "Observable";
};
} // js
} // realm

View File

@ -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<typename T>
class RealmClass : public ClassDefinition<T, SharedRealm> {
class RealmClass : public ClassDefinition<T, SharedRealm, ObservableClass<T>> {
using GlobalContextType = typename T::GlobalContext;
using ContextType = typename T::Context;
using FunctionType = typename T::Function;

View File

@ -30,10 +30,29 @@ namespace realm {
namespace js {
template<typename T>
struct ResultsClass : ClassDefinition<T, realm::Results, CollectionClass<T>> {
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<Query> q = {}, SortOrder s = {}) : realm::Results(r, lv, q, s) {}
std::map<Protected<typename T::Function>, NotificationToken, typename Protected<typename T::Function>::Comparator> m_notification_tokens;
};
template<typename T>
struct ResultsClass : ClassDefinition<T, realm::js::Results<T>, CollectionClass<T>> {
using ContextType = typename T::Context;
using ObjectType = typename T::Object;
using ValueType = typename T::Value;
using FunctionType = typename T::Function;
using Object = js::Object<T>;
using Value = js::Value<T>;
using ReturnValue = js::ReturnValue<T>;
@ -55,6 +74,11 @@ struct ResultsClass : ClassDefinition<T, realm::Results, CollectionClass<T>> {
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<T> const methods = {
@ -62,6 +86,9 @@ struct ResultsClass : ClassDefinition<T, realm::Results, CollectionClass<T>> {
{"filtered", wrap<filtered>},
{"sorted", wrap<sorted>},
{"isValid", wrap<is_valid>},
{"addListener", wrap<add_listener>},
{"removeListener", wrap<remove_listener>},
{"removeAllListeners", wrap<remove_all_listeners>},
};
PropertyMap<T> const properties = {
@ -73,13 +100,13 @@ struct ResultsClass : ClassDefinition<T, realm::Results, CollectionClass<T>> {
template<typename T>
typename T::Object ResultsClass<T>::create_instance(ContextType ctx, realm::Results results) {
return create_object<T, ResultsClass<T>>(ctx, new realm::Results(std::move(results)));
return create_object<T, ResultsClass<T>>(ctx, new realm::js::Results<T>(std::move(results)));
}
template<typename T>
typename T::Object ResultsClass<T>::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<T, ResultsClass<T>>(ctx, new realm::Results(realm, *table));
return create_object<T, ResultsClass<T>>(ctx, new realm::js::Results<T>(realm, *table));
}
template<typename T>
@ -153,7 +180,7 @@ typename T::Object ResultsClass<T>::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<T>(realm, collection.get_query(), {std::move(columns), std::move(ascending)});
return create_object<T, ResultsClass<T>>(ctx, results);
}
@ -206,6 +233,42 @@ template<typename T>
void ResultsClass<T>::is_valid(ContextType ctx, ObjectType this_object, size_t argc, const ValueType arguments[], ReturnValue &return_value) {
return_value.set(get_internal<T, ResultsClass<T>>(this_object)->is_valid());
}
template<typename T>
void ResultsClass<T>::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<T, ResultsClass<T>>(this_object);
auto callback = Value::validated_to_function(ctx, arguments[0]);
Protected<FunctionType> protected_callback(ctx, callback);
Protected<ObjectType> protected_this(ctx, this_object);
Protected<typename T::GlobalContext> protected_ctx(Context<T>::get_global_context(ctx));
auto token = results->add_notification_callback([=](CollectionChangeSet change_set, std::exception_ptr exception) {
ValueType arguments[2];
arguments[0] = static_cast<ObjectType>(protected_this);
arguments[1] = CollectionClass<T>::create_collection_change_set(protected_ctx, change_set);
Function<T>::call(protected_ctx, protected_callback, protected_this, 2, arguments);
});
results->m_notification_tokens.emplace(protected_callback, std::move(token));
}
template<typename T>
void ResultsClass<T>::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<T, ResultsClass<T>>(this_object);
auto callback = Value::validated_to_function(ctx, arguments[0]);
results->m_notification_tokens.erase(Protected<FunctionType>(ctx, callback));
}
template<typename T>
void ResultsClass<T>::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<T, ResultsClass<T>>(this_object);
results->m_notification_tokens.clear();
}
} // js
} // realm

View File

@ -247,6 +247,10 @@ class Protected {
bool operator!=(const ValueType &) const;
bool operator==(const Protected<ValueType> &) const;
bool operator!=(const Protected<ValueType> &) const;
struct Comparator {
bool operator()(const Protected<ValueType>& a, const Protected<ValueType>& b) const;
};
};
template<typename T>

View File

@ -47,6 +47,12 @@ class Protected<JSGlobalContextRef> {
operator bool() const {
return m_context != nullptr;
}
struct Comparator {
bool operator() (const Protected<JSGlobalContextRef>& a, const Protected<JSGlobalContextRef>& b) const {
return a.m_context == b.m_context;
}
};
};
template<>
@ -75,6 +81,15 @@ class Protected<JSValueRef> {
operator bool() const {
return m_value != nullptr;
}
struct Comparator {
bool operator() (const Protected<JSValueRef>& a, const Protected<JSValueRef>& b) const {
if (a.m_context != b.m_context) {
return false;
}
return JSValueIsStrictEqual(a.m_context, a.m_value, b.m_value);
}
};
};
template<>

View File

@ -50,6 +50,10 @@ class Protected {
bool operator!=(const Protected<MemberType> &other) const {
return m_value != other.m_value;
}
struct Comparator {
bool operator()(const Protected<MemberType>& a, const Protected<MemberType>& b) const { return a == b; }
};
};
} // node

View File

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

View File

@ -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];
}

View File

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

View File

@ -1,5 +1,8 @@
{
"name": "realm-tests",
"version": "0.0.1",
"private": true
"private": true,
"dependencies": {
"es6-promise": "^3.2.1"
}
}