diff --git a/CHANGELOG.md b/CHANGELOG.md index e507ec05..66d3a1bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### Enhancements * Support for encryption +* List and Results now inherit from Realm.Collection * Add common Array methods to List and Results * Accept constructor in create() and objects() methods * Support relative paths when opening realms diff --git a/RealmJS.xcodeproj/project.pbxproj b/RealmJS.xcodeproj/project.pbxproj index 3c237af8..f02754be 100644 --- a/RealmJS.xcodeproj/project.pbxproj +++ b/RealmJS.xcodeproj/project.pbxproj @@ -76,6 +76,8 @@ F6BCCFE21C8380A400FE31AE /* lib in Resources */ = {isa = PBXBuildFile; fileRef = F6BCCFDF1C83809A00FE31AE /* lib */; }; F6C74DF01C732CC500C9DDCD /* RealmAnalytics.h in Headers */ = {isa = PBXBuildFile; fileRef = F6C74DEE1C732CC500C9DDCD /* RealmAnalytics.h */; }; F6C74DF11C732CC500C9DDCD /* RealmAnalytics.mm in Sources */ = {isa = PBXBuildFile; fileRef = F6C74DEF1C732CC500C9DDCD /* RealmAnalytics.mm */; }; + F6CB31001C8EDDAB0070EF3F /* js_collection.cpp in Sources */ = {isa = PBXBuildFile; fileRef = F6CB30FC1C8EDD760070EF3F /* js_collection.cpp */; }; + F6CB31011C8EDDBB0070EF3F /* js_collection.hpp in Headers */ = {isa = PBXBuildFile; fileRef = F6CB30FD1C8EDD760070EF3F /* js_collection.hpp */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -247,6 +249,8 @@ F6C3FBBC1BF680EC00E6FFD4 /* json.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = json.hpp; sourceTree = ""; }; F6C74DEE1C732CC500C9DDCD /* RealmAnalytics.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = RealmAnalytics.h; path = "react-native/RealmAnalytics.h"; sourceTree = ""; }; F6C74DEF1C732CC500C9DDCD /* RealmAnalytics.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = RealmAnalytics.mm; path = "react-native/RealmAnalytics.mm"; sourceTree = ""; }; + F6CB30FC1C8EDD760070EF3F /* js_collection.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = js_collection.cpp; sourceTree = ""; }; + F6CB30FD1C8EDD760070EF3F /* js_collection.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = js_collection.hpp; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -309,6 +313,8 @@ F62A35141C18E783004A917D /* Object Store */, 029048011C0428DF00ABDED4 /* js_init.cpp */, 029048021C0428DF00ABDED4 /* js_init.h */, + F6CB30FC1C8EDD760070EF3F /* js_collection.cpp */, + F6CB30FD1C8EDD760070EF3F /* js_collection.hpp */, 029048031C0428DF00ABDED4 /* js_list.cpp */, 029048041C0428DF00ABDED4 /* js_list.hpp */, 029048051C0428DF00ABDED4 /* js_object.cpp */, @@ -535,6 +541,7 @@ 029048181C0428DF00ABDED4 /* js_realm.hpp in Headers */, 029048141C0428DF00ABDED4 /* js_list.hpp in Headers */, F6BB7DF21BF681BC00D0A69E /* base64.hpp in Headers */, + F6CB31011C8EDDBB0070EF3F /* js_collection.hpp in Headers */, 0290481C1C0428DF00ABDED4 /* js_schema.hpp in Headers */, 029048161C0428DF00ABDED4 /* js_object.hpp in Headers */, 029048371C042A3C00ABDED4 /* platform.hpp in Headers */, @@ -787,6 +794,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + F6CB31001C8EDDAB0070EF3F /* js_collection.cpp in Sources */, 02F59EE31C88F2BB007F774C /* transact_log_handler.cpp in Sources */, F63FF2E81C159C4B00B3B8E0 /* platform.mm in Sources */, 02F59EC31C88F17D007F774C /* results.cpp in Sources */, diff --git a/lib/browser/collections.js b/lib/browser/collections.js new file mode 100644 index 00000000..a35dd465 --- /dev/null +++ b/lib/browser/collections.js @@ -0,0 +1,125 @@ +//////////////////////////////////////////////////////////////////////////// +// +// 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. +// +//////////////////////////////////////////////////////////////////////////// + +'use strict'; + +import { keys } from './constants'; +import { getterForProperty, setterForProperty } from './util'; + +let mutationListeners = {}; + +export default class Collection { + constructor() { + throw new TypeError('Illegal constructor'); + } +} + +export function addMutationListener(realmId, callback) { + let listeners = mutationListeners[realmId] || (mutationListeners[realmId] = new Set()); + listeners.add(callback); +} + +export function removeMutationListener(realmId, callback) { + let listeners = mutationListeners[realmId]; + if (listeners) { + listeners.delete(callback); + } +} + +export function clearMutationListeners() { + mutationListeners = {}; +} + +export function fireMutationListeners(realmId) { + let listeners = mutationListeners[realmId]; + if (listeners) { + listeners.forEach((cb) => cb()); + } +} + +export function createCollection(prototype, realmId, info, mutable) { + let collection = Object.create(prototype); + let size = 0; + + Object.defineProperties(collection, { + 'length': { + get: getterForProperty('length'), + }, + '-1': { + value: undefined, + }, + }); + + let resize = function(length) { + if (length == null) { + length = collection.length; + } + if (length == size) { + return; + } + + let props = {}; + + if (length > size) { + for (let i = size; i < length; i++) { + props[i] = { + get: getterForProperty(i), + set: mutable ? setterForProperty(i) : undefined, + enumerable: true, + configurable: true, + }; + } + } + else if (length < size) { + for (let i = size; i >= length; i--) { + delete collection[i]; + } + } + + // Helpfully throw an exception on attempts to set to one past the last index. + props[length] = { + value: undefined, + configurable: true, + }; + + Object.defineProperties(collection, props); + + size = length; + }; + + collection[keys.realm] = realmId; + collection[keys.id] = info.id; + collection[keys.type] = info.type; + + resize(info.size); + + addMutationListener(realmId, function listener() { + try { + resize(); + } catch (e) { + // If the error indicates the collection was deleted, then remove this listener. + if (e.message.indexOf('is not attached') > 0) { + removeMutationListener(realmId, listener); + } else { + throw e; + } + } + }); + + return collection; +} diff --git a/lib/browser/index.js b/lib/browser/index.js index 27eb76c2..23087816 100644 --- a/lib/browser/index.js +++ b/lib/browser/index.js @@ -20,18 +20,19 @@ import { NativeModules } from 'react-native'; import { keys, propTypes, objectTypes } from './constants'; -import * as lists from './lists'; +import Collection, * as collections from './collections'; +import List, { createList } from './lists'; +import Results, { createResults } from './results'; import * as objects from './objects'; -import * as results from './results'; import * as rpc from './rpc'; import * as util from './util'; const {debugHosts, debugPort} = NativeModules.Realm; const listenersKey = Symbol(); -rpc.registerTypeConverter(objectTypes.LIST, lists.create); +rpc.registerTypeConverter(objectTypes.LIST, createList); +rpc.registerTypeConverter(objectTypes.RESULTS, createResults); rpc.registerTypeConverter(objectTypes.OBJECT, objects.create); -rpc.registerTypeConverter(objectTypes.RESULTS, results.create); export default class Realm { constructor(config) { @@ -137,7 +138,7 @@ export default class Realm { callback(); } catch (e) { rpc.cancelTransaction(realmId); - util.fireMutationListeners(realmId); + collections.fireMutationListeners(realmId); throw e; } @@ -159,11 +160,14 @@ util.createMethods(Realm.prototype, objectTypes.REALM, [ ], true); Object.defineProperties(Realm, { + Collection: { + value: Collection, + }, List: { - value: lists.List, + value: List, }, Results: { - value: results.Results, + value: Results, }, Types: { value: Object.freeze(propTypes), @@ -174,7 +178,7 @@ Object.defineProperties(Realm, { }, clearTestState: { value: function() { - util.clearMutationListeners(); + collections.clearMutationListeners(); rpc.clearTestState(); }, }, diff --git a/lib/browser/lists.js b/lib/browser/lists.js index 23d2eb32..e0644c7c 100644 --- a/lib/browser/lists.js +++ b/lib/browser/lists.js @@ -18,13 +18,11 @@ 'use strict'; +import Collection, { createCollection } from './collections'; import { objectTypes } from './constants'; -import { createCollection, createMethods } from './util'; +import { createMethods } from './util'; -export class List { - constructor() { - throw new TypeError('Illegal constructor'); - } +export default class List extends Collection { } // Non-mutating methods: @@ -43,6 +41,6 @@ createMethods(List.prototype, objectTypes.LIST, [ 'splice', ], true); -export function create(realmId, info) { +export function createList(realmId, info) { return createCollection(List.prototype, realmId, info, true); } diff --git a/lib/browser/results.js b/lib/browser/results.js index 13717328..203135a0 100644 --- a/lib/browser/results.js +++ b/lib/browser/results.js @@ -18,13 +18,11 @@ 'use strict'; +import Collection, { createCollection } from './collections'; import { objectTypes } from './constants'; -import { createCollection, createMethods } from './util'; +import { createMethods } from './util'; -export class Results { - constructor() { - throw new TypeError('Illegal constructor'); - } +export default class Results extends Collection { } createMethods(Results.prototype, objectTypes.RESULTS, [ @@ -33,6 +31,6 @@ createMethods(Results.prototype, objectTypes.RESULTS, [ 'snapshot', ]); -export function create(realmId, info) { +export function createResults(realmId, info) { return createCollection(Results.prototype, realmId, info); } diff --git a/lib/browser/util.js b/lib/browser/util.js index 683a4c25..2e89cdf2 100644 --- a/lib/browser/util.js +++ b/lib/browser/util.js @@ -18,106 +18,10 @@ 'use strict'; +import { fireMutationListeners } from './collections'; import { keys } from './constants'; import * as rpc from './rpc'; -let mutationListeners = {}; - -function addMutationListener(realmId, callback) { - let listeners = mutationListeners[realmId] || (mutationListeners[realmId] = new Set()); - listeners.add(callback); -} - -function removeMutationListener(realmId, callback) { - let listeners = mutationListeners[realmId]; - if (listeners) { - listeners.delete(callback); - } -} - -export function clearMutationListeners() { - mutationListeners = {}; -} - -export function fireMutationListeners(realmId) { - let listeners = mutationListeners[realmId]; - if (listeners) { - listeners.forEach((cb) => cb()); - } -} - -export function createCollection(prototype, realmId, info, mutable) { - let list = Object.create(prototype); - let size = 0; - - Object.defineProperties(list, { - 'length': { - get: getterForProperty('length'), - }, - '-1': { - value: undefined, - }, - }); - - let resize = function(length) { - if (length == null) { - length = list.length; - } - if (length == size) { - return; - } - - let props = {}; - - if (length > size) { - for (let i = size; i < length; i++) { - props[i] = { - get: getterForProperty(i), - set: mutable ? setterForProperty(i) : undefined, - enumerable: true, - configurable: true, - }; - } - } - else if (length < size) { - for (let i = size; i >= length; i--) { - delete list[i]; - } - } - - // Helpfully throw an exception on attempts to set to list[list.length]. - props[length] = { - value: undefined, - configurable: true, - }; - - Object.defineProperties(list, props); - - size = length; - }; - - list[keys.realm] = realmId; - list[keys.id] = info.id; - list[keys.type] = info.type; - - resize(info.size); - - addMutationListener(realmId, function listener() { - try { - resize(); - } catch (e) { - // If the error indicates the list was deleted, then remove this listener. - if (e.message.indexOf('is not attached') > 0) { - removeMutationListener(realmId, listener); - } else { - throw e; - } - } - }); - - return list; -} - export function createMethods(prototype, type, methodNames, mutates) { let props = {}; diff --git a/lib/index.js b/lib/index.js index fec65d35..87b79b80 100644 --- a/lib/index.js +++ b/lib/index.js @@ -31,8 +31,7 @@ if (typeof Realm != 'undefined') { throw new Error('Missing Realm constructor - please ensure RealmReact framework is included!'); } -// Add the specified Array methods to the prototype of List and Results. -Object.defineProperties(realmConstructor.List.prototype, arrayMethods); -Object.defineProperties(realmConstructor.Results.prototype, arrayMethods); +// Add the specified Array methods to the Collection prototype. +Object.defineProperties(realmConstructor.Collection.prototype, arrayMethods); module.exports = realmConstructor; diff --git a/src/js_collection.cpp b/src/js_collection.cpp new file mode 100644 index 00000000..968404f2 --- /dev/null +++ b/src/js_collection.cpp @@ -0,0 +1,30 @@ +//////////////////////////////////////////////////////////////////////////// +// +// 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. +// +//////////////////////////////////////////////////////////////////////////// + +#include "js_collection.hpp" + +static JSClassRef RJSCreateCollectionClass() { + JSClassDefinition classDefinition = kJSClassDefinitionEmpty; + classDefinition.className = "Collection"; + return JSClassCreate(&classDefinition); +} + +JSClassRef RJSCollectionClass() { + static JSClassRef s_collectionClass = RJSCreateCollectionClass(); + return s_collectionClass; +} diff --git a/src/js_collection.hpp b/src/js_collection.hpp new file mode 100644 index 00000000..ee21c391 --- /dev/null +++ b/src/js_collection.hpp @@ -0,0 +1,23 @@ +//////////////////////////////////////////////////////////////////////////// +// +// 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_util.hpp" + +JSClassRef RJSCollectionClass(); diff --git a/src/js_init.cpp b/src/js_init.cpp index c25e84f8..3c1623c4 100644 --- a/src/js_init.cpp +++ b/src/js_init.cpp @@ -19,6 +19,7 @@ #include "js_init.h" #include "js_realm.hpp" #include "js_object.hpp" +#include "js_collection.hpp" #include "js_list.hpp" #include "js_results.hpp" #include "js_util.hpp" @@ -69,6 +70,7 @@ static JSValueRef ClearTestState(JSContextRef ctx, JSObjectRef function, JSObjec JSObjectRef RJSConstructorCreate(JSContextRef ctx) { static JSStringRef clearTestStateString = JSStringCreateWithUTF8CString("clearTestState"); + static JSStringRef collectionString = JSStringCreateWithUTF8CString("Collection"); static JSStringRef listString = JSStringCreateWithUTF8CString("List"); static JSStringRef resultsString = JSStringCreateWithUTF8CString("Results"); static JSStringRef typeString = JSStringCreateWithUTF8CString("Types"); @@ -76,6 +78,9 @@ JSObjectRef RJSConstructorCreate(JSContextRef ctx) { JSObjectRef realmObject = JSObjectMake(ctx, RJSRealmConstructorClass(), NULL); JSPropertyAttributes attributes = kJSPropertyAttributeReadOnly | kJSPropertyAttributeDontEnum | kJSPropertyAttributeDontDelete; + JSObjectRef collectionConstructor = JSObjectMakeConstructor(ctx, RJSCollectionClass(), UncallableConstructor); + RJSValidatedSetProperty(ctx, realmObject, collectionString, collectionConstructor, attributes); + JSObjectRef listConstructor = JSObjectMakeConstructor(ctx, RJSListClass(), UncallableConstructor); RJSValidatedSetProperty(ctx, realmObject, listString, listConstructor, attributes); diff --git a/src/js_list.cpp b/src/js_list.cpp index 7acbdadf..6a1321f9 100644 --- a/src/js_list.cpp +++ b/src/js_list.cpp @@ -17,6 +17,7 @@ //////////////////////////////////////////////////////////////////////////// #include "js_list.hpp" +#include "js_collection.hpp" #include "js_object.hpp" #include "js_results.hpp" #include "js_util.hpp" @@ -271,6 +272,6 @@ static const JSStaticFunction RJSListFuncs[] = { }; JSClassRef RJSListClass() { - static JSClassRef s_listClass = RJSCreateWrapperClass("RealmList", ListGetProperty, ListSetProperty, RJSListFuncs, ListPropertyNames); + static JSClassRef s_listClass = RJSCreateWrapperClass("List", ListGetProperty, ListSetProperty, RJSListFuncs, ListPropertyNames, RJSCollectionClass()); return s_listClass; } diff --git a/src/js_results.cpp b/src/js_results.cpp index cd85c123..a332f224 100644 --- a/src/js_results.cpp +++ b/src/js_results.cpp @@ -17,6 +17,7 @@ //////////////////////////////////////////////////////////////////////////// #include "js_results.hpp" +#include "js_collection.hpp" #include "js_object.hpp" #include "object_accessor.hpp" #include "results.hpp" @@ -243,6 +244,6 @@ static const JSStaticFunction RJSResultsFuncs[] = { }; JSClassRef RJSResultsClass() { - static JSClassRef s_objectClass = RJSCreateWrapperClass("Results", ResultsGetProperty, ResultsSetProperty, RJSResultsFuncs, ResultsPropertyNames); + static JSClassRef s_objectClass = RJSCreateWrapperClass("Results", ResultsGetProperty, ResultsSetProperty, RJSResultsFuncs, ResultsPropertyNames, RJSCollectionClass()); return s_objectClass; } diff --git a/src/js_util.hpp b/src/js_util.hpp index 4d7fa311..0b219337 100644 --- a/src/js_util.hpp +++ b/src/js_util.hpp @@ -54,7 +54,7 @@ inline T RJSGetInternal(JSObjectRef jsObject) { template JSClassRef RJSCreateWrapperClass(const char * name, JSObjectGetPropertyCallback getter = NULL, JSObjectSetPropertyCallback setter = NULL, const JSStaticFunction *funcs = NULL, - JSObjectGetPropertyNamesCallback propertyNames = NULL) { + JSObjectGetPropertyNamesCallback propertyNames = NULL, JSClassRef parentClass = NULL) { JSClassDefinition classDefinition = kJSClassDefinitionEmpty; classDefinition.className = name; classDefinition.finalize = RJSFinalize; @@ -62,6 +62,7 @@ JSClassRef RJSCreateWrapperClass(const char * name, JSObjectGetPropertyCallback classDefinition.setProperty = setter; classDefinition.staticFunctions = funcs; classDefinition.getPropertyNames = propertyNames; + classDefinition.parentClass = parentClass; return JSClassCreate(&classDefinition); } diff --git a/tests/js/list-tests.js b/tests/js/list-tests.js index 0a93c913..2d9a66d9 100644 --- a/tests/js/list-tests.js +++ b/tests/js/list-tests.js @@ -30,6 +30,7 @@ module.exports = BaseTest.extend({ realm.write(function() { var obj = realm.create('PersonList', {list: []}); TestCase.assertTrue(obj.list instanceof Realm.List); + TestCase.assertTrue(obj.list instanceof Realm.Collection); }); TestCase.assertThrows(function() { diff --git a/tests/js/results-tests.js b/tests/js/results-tests.js index b515a337..3470a267 100644 --- a/tests/js/results-tests.js +++ b/tests/js/results-tests.js @@ -29,6 +29,7 @@ module.exports = BaseTest.extend({ var objects = realm.objects('TestObject'); TestCase.assertTrue(objects instanceof Realm.Results); + TestCase.assertTrue(objects instanceof Realm.Collection); TestCase.assertThrows(function() { new Realm.Results();