From ae93cf665dfcb6915bbfbd10375ef987293fc3b6 Mon Sep 17 00:00:00 2001 From: "rossberg@chromium.org" Date: Thu, 25 Oct 2012 14:56:44 +0000 Subject: [PATCH] Initial JS stub implementation of Object.observe. Adds support for .object/.unobserve/.notify/.deliverChangeRecords. No delivery mechanism is implemented for end-of-microtask. Review URL: https://codereview.chromium.org/11225058 Patch from Rafael Weinstein . git-svn-id: http://v8.googlecode.com/svn/branches/bleeding_edge@12820 ce2b1a6d-e550-0410-aec6-3dcde31c8c00 --- src/object-observe.js | 164 +++++++++++++++++ test/mjsunit/harmony/object-observe.js | 241 +++++++++++++++++++++++++ 2 files changed, 405 insertions(+) create mode 100644 src/object-observe.js create mode 100644 test/mjsunit/harmony/object-observe.js diff --git a/src/object-observe.js b/src/object-observe.js new file mode 100644 index 0000000000..dcf98d84ae --- /dev/null +++ b/src/object-observe.js @@ -0,0 +1,164 @@ +// Copyright 2012 the V8 project authors. All rights reserved. +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following +// disclaimer in the documentation and/or other materials provided +// with the distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived +// from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +"use strict"; + +var InternalObjectIsFrozen = $Object.isFrozen; +var InternalObjectFreeze = $Object.freeze; + +var InternalWeakMapProto = { + __proto__: null, + set: $WeakMap.prototype.set, + get: $WeakMap.prototype.get, + has: $WeakMap.prototype.has +} + +function createInternalWeakMap() { + var map = new $WeakMap; + map.__proto__ = InternalWeakMapProto; + return map; +} + +var observerInfoMap = createInternalWeakMap(); +var objectInfoMap = createInternalWeakMap(); + +function ObjectObserve(object, callback) { + if (!IS_SPEC_OBJECT(object)) + throw MakeTypeError("observe_non_object", ["observe"]); + if (!IS_SPEC_FUNCTION(callback)) + throw MakeTypeError("observe_non_function", ["observe"]); + if (InternalObjectIsFrozen(callback)) + throw MakeTypeError("observe_callback_frozen"); + + if (!observerInfoMap.has(callback)) { + // TODO: setup observerInfo.priority. + observerInfoMap.set(callback, { + pendingChangeRecords: null + }); + } + + var objectInfo = objectInfoMap.get(object); + if (IS_UNDEFINED(objectInfo)) { + // TODO: setup objectInfo.notifier + objectInfo = { + changeObservers: new InternalArray(callback) + }; + objectInfoMap.set(object, objectInfo); + return; + } + + var changeObservers = objectInfo.changeObservers; + if (changeObservers.indexOf(callback) >= 0) + return; + + changeObservers.push(callback); +} + +function ObjectUnobserve(object, callback) { + if (!IS_SPEC_OBJECT(object)) + throw MakeTypeError("observe_non_object", ["unobserve"]); + + var objectInfo = objectInfoMap.get(object); + if (IS_UNDEFINED(objectInfo)) + return; + + var changeObservers = objectInfo.changeObservers; + var index = changeObservers.indexOf(callback); + if (index < 0) + return; + + changeObservers.splice(index, 1); +} + +function EnqueueChangeRecord(changeRecord, observers) { + for (var i = 0; i < observers.length; i++) { + var observer = observers[i]; + var observerInfo = observerInfoMap.get(observer); + + // TODO: "activate" the observer + + if (IS_NULL(observerInfo.pendingChangeRecords)) { + observerInfo.pendingChangeRecords = new InternalArray(changeRecord); + } else { + observerInfo.pendingChangeRecords.push(changeRecord); + } + } +} + +function ObjectNotify(object, changeRecord) { + // TODO: notifier needs to be [[THIS]] + if (!IS_STRING(changeRecord.type)) + throw MakeTypeError("observe_type_non_string"); + + var objectInfo = objectInfoMap.get(object); + if (IS_UNDEFINED(objectInfo)) + return; + + var newRecord = { + object: object // TODO: Needs to be 'object' retreived from notifier + }; + for (var prop in changeRecord) { + if (prop === 'object') + continue; + newRecord[prop] = changeRecord[prop]; + } + InternalObjectFreeze(newRecord); + + EnqueueChangeRecord(newRecord, objectInfo.changeObservers); +} + +function ObjectDeliverChangeRecords(callback) { + if (!IS_SPEC_FUNCTION(callback)) + throw MakeTypeError("observe_non_function", ["deliverChangeRecords"]); + + var observerInfo = observerInfoMap.get(callback); + if (IS_UNDEFINED(observerInfo)) + return; + + var pendingChangeRecords = observerInfo.pendingChangeRecords; + if (IS_NULL(pendingChangeRecords)) + return; + + observerInfo.pendingChangeRecords = null; + var delivered = []; + %MoveArrayContents(pendingChangeRecords, delivered); + try { + %Call(void 0, delivered, callback); + } catch (ex) {} +} + +function SetupObjectObserve() { + %CheckIsBootstrapping(); + InstallFunctions($Object, DONT_ENUM, $Array( + "deliverChangeRecords", ObjectDeliverChangeRecords, + "notify", ObjectNotify, // TODO: Remove when getNotifier is implemented. + "observe", ObjectObserve, + "unobserve", ObjectUnobserve + )); +} + +SetupObjectObserve(); \ No newline at end of file diff --git a/test/mjsunit/harmony/object-observe.js b/test/mjsunit/harmony/object-observe.js new file mode 100644 index 0000000000..07656d35db --- /dev/null +++ b/test/mjsunit/harmony/object-observe.js @@ -0,0 +1,241 @@ +// Copyright 2012 the V8 project authors. All rights reserved. +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following +// disclaimer in the documentation and/or other materials provided +// with the distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived +// from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +// Flags: --harmony-object-observe + +var allObservers = []; +function reset() { + allObservers.forEach(function(observer) { observer.reset(); }); +} + +function createObserver() { + "use strict"; // So that |this| in callback can be undefined. + + var observer = { + records: undefined, + callbackCount: 0, + reset: function() { + this.records = undefined; + this.callbackCount = 0; + }, + assertNotCalled: function() { + assertEquals(undefined, this.records); + assertEquals(0, this.callbackCount); + }, + assertCalled: function() { + assertEquals(1, this.callbackCount); + }, + assertRecordCount: function(count) { + this.assertCalled(); + assertEquals(count, this.records.length); + }, + assertCallbackRecords: function(recs) { + this.assertRecordCount(recs.length); + for (var i = 0; i < recs.length; i++) { + assertSame(this.records[i].object, recs[i].object); + assertEquals('string', typeof recs[i].type); + assertPropertiesEqual(this.records[i], recs[i]); + } + } + }; + + observer.callback = function(r) { + assertEquals(undefined, this); + assertEquals('object', typeof r); + assertTrue(r instanceof Array) + observer.records = r; + observer.callbackCount++; + }; + + observer.reset(); + allObservers.push(observer); + return observer; +} + +var observer = createObserver(); +assertEquals("function", typeof observer.callback); +var obj = {}; + +function frozenFunction() {} +Object.freeze(frozenFunction); +var nonFunction = {}; +var changeRecordWithAccessor = { type: 'foo' }; +var recordCreated = false; +Object.defineProperty(changeRecordWithAccessor, 'name', { + get: function() { + recordCreated = true; + }, + enumerable: true +}) + +// Object.observe +assertThrows(function() { Object.observe("non-object", observer.callback); }, TypeError); +assertThrows(function() { Object.observe(obj, nonFunction); }, TypeError); +assertThrows(function() { Object.observe(obj, frozenFunction); }, TypeError); + +// Object.unobserve +assertThrows(function() { Object.unobserve(4, observer.callback); }, TypeError); + +// Object.notify +assertThrows(function() { Object.notify(obj, {}); }, TypeError); +assertThrows(function() { Object.notify(obj, { type: 4 }); }, TypeError); +Object.notify(obj, changeRecordWithAccessor); +assertFalse(recordCreated); + +// Object.deliverChangeRecords +assertThrows(function() { Object.deliverChangeRecords(nonFunction); }, TypeError); + +// Multiple records are delivered. +Object.observe(obj, observer.callback); +Object.notify(obj, { + object: obj, + type: 'updated', + name: 'foo', + expando: 1 +}); + +Object.notify(obj, { + object: obj, + type: 'deleted', + name: 'bar', + expando2: 'str' +}); +Object.deliverChangeRecords(observer.callback); +observer.assertCallbackRecords([ + { object: obj, name: 'foo', type: 'updated', expando: 1 }, + { object: obj, name: 'bar', type: 'deleted', expando2: 'str' } +]); + +// No delivery takes place if no records are pending +reset(); +Object.deliverChangeRecords(observer.callback); +observer.assertNotCalled(); + +// Multiple observation has no effect. +reset(); +Object.observe(obj, observer.callback); +Object.observe(obj, observer.callback); +Object.notify(obj, { + type: 'foo', +}); +Object.deliverChangeRecords(observer.callback); +observer.assertCalled(); + +// Observation can be stopped. +reset(); +Object.unobserve(obj, observer.callback); +Object.notify(obj, { + type: 'foo', +}); +Object.deliverChangeRecords(observer.callback); +observer.assertNotCalled(); + +// Multiple unobservation has no effect +reset(); +Object.unobserve(obj, observer.callback); +Object.unobserve(obj, observer.callback); +Object.notify(obj, { + type: 'foo', +}); +Object.deliverChangeRecords(observer.callback); +observer.assertNotCalled(); + +// Re-observation works and only includes changeRecords after of call. +reset(); +Object.notify(obj, { + type: 'foo', +}); +Object.observe(obj, observer.callback); +Object.notify(obj, { + type: 'foo', +}); +records = undefined; +Object.deliverChangeRecords(observer.callback); +observer.assertRecordCount(1); + +// Observing a continuous stream of changes, while itermittantly unobserving. +reset(); +Object.observe(obj, observer.callback); +Object.notify(obj, { + type: 'foo', + val: 1 +}); + +Object.unobserve(obj, observer.callback); +Object.notify(obj, { + type: 'foo', + val: 2 +}); + +Object.observe(obj, observer.callback); +Object.notify(obj, { + type: 'foo', + val: 3 +}); + +Object.unobserve(obj, observer.callback); +Object.notify(obj, { + type: 'foo', + val: 4 +}); + +Object.observe(obj, observer.callback); +Object.notify(obj, { + type: 'foo', + val: 5 +}); + +Object.unobserve(obj, observer.callback); +Object.deliverChangeRecords(observer.callback); +observer.assertCallbackRecords([ + { object: obj, type: 'foo', val: 1 }, + { object: obj, type: 'foo', val: 3 }, + { object: obj, type: 'foo', val: 5 } +]); + +// Observing multiple objects; records appear in order;. +reset(); +var obj2 = {}; +var obj3 = {} +Object.observe(obj, observer.callback); +Object.observe(obj3, observer.callback); +Object.observe(obj2, observer.callback); +Object.notify(obj, { + type: 'foo', +}); +Object.notify(obj2, { + type: 'foo', +}); +Object.notify(obj3, { + type: 'foo', +}); +Object.deliverChangeRecords(observer.callback); +observer.assertCallbackRecords([ + { object: obj, type: 'foo' }, + { object: obj2, type: 'foo' }, + { object: obj3, type: 'foo' } +]); \ No newline at end of file