Permalink
Browse files

added dispatcher util class. fixed bug where disconnect would always …

…return the old summaries object
  • Loading branch information...
1 parent 6bdaa8c commit 450d15b6f260c79b0975eb660c0680b460b86f59 @rafaelw rafaelw committed Aug 27, 2012
Showing with 462 additions and 4 deletions.
  1. +9 −2 change_summary.js
  2. +2 −2 test.js
  3. +235 −0 util/dispatcher.js
  4. +216 −0 util/dispatcher_test.html
View
11 change_summary.js
@@ -254,8 +254,10 @@
if (results.length)
summaries = results;
- if (!isDisconnecting && summaries)
+ if (!isDisconnecting && summaries) {
callback(summaries);
+ summaries = undefined;
+ }
} catch (ex) {
console.error(ex);
@@ -386,7 +388,12 @@
});
observing = false;
- return summaries;
+
+ if (!summaries)
+ return;
+ var retval = summaries;
+ summaries = undefined;
+ return retval;
};
this.reconnect = function() {
View
4 test.js
@@ -43,8 +43,8 @@ function setUp() {
}
function tearDown() {
- observer.disconnect();
- summaries = undefined;
+ summaries = observer.disconnect();
+ assertUndefined(summaries);
callbackCount = 0;
expectedCallbackCount = 0;
}
View
235 util/dispatcher.js
@@ -0,0 +1,235 @@
+// Copyright 2012 Google 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.
+
+/*
+ * NOTE: ChangeSummaryDispatcher is a utility class which allows a functional-style
+ * registry for observation using ChangeSummary.
+ *
+ * It's important to understand that callbacks which are registered on the same
+ * ChangeSummaryObserver will *not* be isolated from each other. That is, if
+ * one callback makes any changes which affect observations, another callback
+ * registerd on the same dispatcher, will be ignorant of those changes.
+ *
+ * If you're use case requires that callbacks be isolated from each other, use multiple
+ * dipatchers.
+ */
+
+(function(global) {
+
+ // FIXME: Use Map/Set iterators when available.
+ if (!global.Map || !global.Set)
+ throw Error('ChangeSummary requires harmony Maps and Sets');
+
+ var HarmonyMap = global.Map;
+ var HarmonySet = global.Set;
+
+ function Map() {
+ this.map_ = new HarmonyMap;
+ this.keys_ = [];
+ }
+
+ Map.prototype = {
+ get: function(key) {
+ return this.map_.get(key);
+ },
+
+ set: function(key, value) {
+ if (!this.map_.has(key))
+ this.keys_.push(key);
+ return this.map_.set(key, value);
+ },
+
+ has: function(key) {
+ return this.map_.has(key);
+ },
+
+ delete: function(key) {
+ this.keys_.splice(this.keys_.indexOf(key), 1);
+ this.map_.delete(key);
+ },
+
+ keys: function() {
+ return this.keys_.slice();
+ }
+ }
+
+ function Set() {
+ this.set_ = new HarmonySet;
+ this.keys_ = [];
+ }
+
+ Set.prototype = {
+ add: function(key) {
+ if (!this.set_.has(key))
+ this.keys_.push(key);
+ return this.set_.add(key);
+ },
+
+ has: function(key) {
+ return this.set_.has(key);
+ },
+
+ delete: function(key) {
+ this.keys_.splice(this.keys_.indexOf(key), 1);
+ this.set_.delete(key);
+ },
+
+ keys: function() {
+ return this.keys_.slice();
+ }
+ }
+
+ function ChangeSummaryDispatcher() {
+ var pathValueObserverMap = new Map;
+ var propertySetObserverMap = new Map;
+
+ function dispatchPathValueObservers(pathValueObservers, summary) {
+ if (!summary.pathValueChanged)
+ return;
+
+ summary.pathValueChanged.forEach(function(pathString) {
+ var callbackSet = pathValueObservers[pathString];
+ if (!callbackSet)
+ return;
+
+ callbackSet.keys().forEach(function(callback) {
+ try {
+ callback(summary.getNewPathValue(pathString), summary.getOldPathValue(pathString));
+ } catch (ex) {
+ console.error('Exception during dispatch: ', ex);
+ if (ChangeSummaryDispatcher.rethrowExceptions)
+ throw ex;
+ }
+ });
+ });
+ }
+
+ function dispatchPropertySetObservers(callbackSet, summary) {
+ var newProperties = summary.newProperties || [];
+ var deletedProperties = summary.deletedProperties || [];
+ var arraySplices = summary.arraySplices || [];
+ if (!newProperties.length && !deletedProperties.length && !arraySplices.length)
+ return;
+
+ callbackSet.keys().forEach(function(callback) {
+ try {
+ callback(newProperties, deletedProperties, arraySplices);
+ } catch (ex) {
+ console.error('Exception during dispatch: ', ex)
+ if (ChangeSummaryDispatcher.rethrowExceptions)
+ throw ex;
+ }
+ });
+ }
+
+ function dispatch(summaries) {
+ summaries.forEach(function(summary) {
+ dispatchPathValueObservers(pathValueObserverMap.get(summary.object), summary);
+ dispatchPropertySetObservers(propertySetObserverMap.get(summary.object), summary);
+ });
+ }
+
+ var observer = new ChangeSummary(dispatch);
+
+ // callback(newValue, oldValue);
+ this.observePathValue = function(obj, pathString, callback) {
+ if (typeof callback != 'function')
+ throw Error('callback must be a function.');
+
+ var pathValueObservers = pathValueObserverMap.get(obj);
+ if (!pathValueObservers) {
+ pathValueObservers = {};
+ pathValueObserverMap.set(obj, pathValueObservers);
+ }
+
+ var callbackSet = pathValueObservers[pathString];
+ if (!callbackSet) {
+ callbackSet = new Set;
+ pathValueObservers[pathString] = callbackSet;
+ observer.observePathValue(obj, pathString);
+ };
+
+ callbackSet.add(callback);
+ };
+
+ this.unobservePathValue = function(obj, pathString, callback) {
+ if (typeof callback != 'function')
+ throw Error('callback must be a function.');
+
+ var pathValueObservers = pathValueObserverMap.get(obj);
+ if (!pathValueObservers)
+ return;
+
+ var callbackSet = pathValueObservers[pathString];
+ if (!callbackSet)
+ return;
+
+ callbackSet.delete(callback);
+ if (!callbackSet.keys().length) {
+ delete pathValueObservers[pathString];
+ observer.unobservePathValue(obj, pathString);
+
+ if (!Object.keys(pathValueObservers).length)
+ pathValueObserverMap.delete(obj);
+ }
+ };
+
+ // callback(newProperties, deletedProperties, arraySplices)
+ this.observePropertySet = function(obj, callback) {
+ if (typeof callback != 'function')
+ throw Error('callback must be a function.');
+
+ var callbackSet = propertySetObserverMap.get(obj);
+ if (!callbackSet) {
+ callbackSet = new Set;
+ propertySetObserverMap.set(obj, callbackSet);
+ observer.observePropertySet(obj);
+ }
+
+ callbackSet.add(callback);
+ };
+
+ this.unobservePropertySet = function(obj, callback) {
+ if (typeof callback != 'function')
+ throw Error('callback must be a function.');
+
+ var callbackSet = propertySetObserverMap.get(obj);
+ if (!callbackSet)
+ return;
+
+ callbackSet.delete(callback);
+ if (!callbackSet.keys().length) {
+ observer.unobservePropertySet(obj);
+ propertySetObserverMap.delete(obj);
+ }
+ };
+
+ this.deliver = function() {
+ observer.deliver();
+ };
+
+ this.disconnect = function() {
+ var summaries = observer.disconnect();
+ if (!summaries || !summaries.length)
+ return;
+ dispatch(summaries);
+ };
+
+ this.reconnect = function() {
+ observer.reconnect();
+ };
+ }
+
+ global.ChangeSummaryDispatcher = ChangeSummaryDispatcher;
+})(window);
View
216 util/dispatcher_test.html
@@ -0,0 +1,216 @@
+<html>
+<!--
+Copyright 2012 Google 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.
+-->
+<head>
+<title>ChangeSummaryDispatcher test</title>
+</head>
+<body>
+<script src="http://closure-library.googlecode.com/svn/trunk/closure/goog/base.js"></script>
+<script src="../change_summary.js"></script>
+<script src="dispatcher.js"></script>
+<body>
+<script>
+"use strict";
+
+goog.require('goog.testing.jsunit');
+
+ChangeSummaryDispatcher.rethrowExceptions = true;
+
+var dispatcher;
+
+function setUp() {
+ dispatcher = new ChangeSummaryDispatcher();
+}
+
+function tearDown() {
+ dispatcher.disconnect();
+}
+
+function assertArraysEquivalent() {
+ var msg = '', a, b;
+ if (arguments.length == 3) {
+ msg = arguments[0];
+ a = arguments[1];
+ b = arguments[2];
+ } else {
+ a = arguments[0];
+ b = arguments[1];
+ }
+ if (a === b)
+ return;
+ assertEquals('different type', typeof a, typeof b);
+ assertEquals('different length', a.length, b.length);
+ for (var i = 0; i < a.length; i++) {
+ assertEquals('index ' + i, a[i], b[i]);
+ }
+}
+
+function assertSplicesEqual(expect, actual) {
+ if (expect === actual)
+ return;
+ assertEquals(expect.length, actual.length);
+ expect.forEach(function(splice, index) {
+ var actualSplice = actual[index];
+ assertEquals(splice.index, actualSplice.index);
+ assertArraysEquivalent(splice.removed, actualSplice.removed);
+ assertEquals(splice.addedCount, actualSplice.addedCount);
+ });
+}
+
+function testPathValue() {
+ var model = {
+ a: {}
+ }
+
+ var expectOldValue;
+ var expectNewValue;
+
+ var count1 = 0;
+ var callback1 = function(newValue, oldValue) {
+ count1++;
+ assertEquals(expectNewValue, newValue);
+ assertEquals(expectOldValue, oldValue);
+ };
+
+ var count2 = 0;
+ var callback2 = function(newValue, oldValue) {
+ count2++;
+ assertEquals(expectNewValue, newValue);
+ assertEquals(expectOldValue, oldValue);
+ };
+
+ dispatcher.observePathValue(model, 'a.b', callback1);
+
+ expectNewValue = 2;
+
+ model.a.b = 2;
+ dispatcher.deliver();
+ assertEquals(1, count1);
+
+ dispatcher.observePathValue(model, 'a.b', callback2);
+
+ expectNewValue = 3;
+ expectOldValue = 2;
+ model.a.b = 3;
+ dispatcher.deliver();
+ assertEquals(2, count1);
+ assertEquals(1, count2);
+
+ expectNewValue = 4;
+ expectOldValue = 3;
+ model.a.b = 4;
+ dispatcher.deliver();
+ assertEquals(3, count1);
+ assertEquals(2, count2);
+
+ dispatcher.unobservePathValue(model, 'a.b', callback1);
+
+ expectNewValue = 5;
+ expectOldValue = 4;
+ model.a.b = 5;
+ dispatcher.deliver();
+ assertEquals(3, count1);
+ assertEquals(3, count2);
+
+ dispatcher.unobservePathValue(model, 'a.b', callback2);
+
+ model.a.b = 5;
+ dispatcher.deliver();
+ assertEquals(3, count1);
+ assertEquals(3, count2);
+
+ model.a.b = 6;
+}
+
+function testPropertySet() {
+ var model = [];
+
+ var expectNewProperties = [];
+ var expectDeletedProperties = [];
+ var expectArraySplices = [];
+
+ var count1 = 0;
+ var callback1 = function(newProperties, deletedProperties, arraySplices) {
+ count1++;
+ assertArraysEquivalent(expectNewProperties, newProperties);
+ assertArraysEquivalent(expectDeletedProperties, deletedProperties);
+ assertSplicesEqual(expectArraySplices, arraySplices);
+ };
+
+ var count2 = 0;
+ var callback2 = function(newProperties, deletedProperties, arraySplices) {
+ count2++;
+ assertArraysEquivalent(expectNewProperties, newProperties);
+ assertArraysEquivalent(expectDeletedProperties, deletedProperties);
+ assertSplicesEqual(expectArraySplices, arraySplices);
+ };
+
+ dispatcher.observePropertySet(model, callback1);
+
+ expectNewProperties = ['foo'];
+ expectArraySplices = [{
+ index: 0,
+ removed: [],
+ addedCount: 1
+ }];
+ model.push(1);
+ model.foo = 'bar';
+ dispatcher.deliver();
+ assertEquals(1, count1);
+ assertEquals(0, count2);
+
+ dispatcher.observePropertySet(model, callback2);
+
+ expectNewProperties = ['bar'];
+ expectArraySplices = [{
+ index: 0,
+ removed: [1],
+ addedCount: 1
+ }];
+ model.splice(0, 1, 2);
+ model.bar = 'bat';
+ dispatcher.deliver();
+ assertEquals(2, count1);
+ assertEquals(1, count2);
+
+ dispatcher.unobservePropertySet(model, callback1);
+
+ expectNewProperties = [];
+ expectDeletedProperties = ['foo'];
+ expectArraySplices = [{
+ index: 0,
+ removed: [],
+ addedCount: 1
+ }];
+ model.unshift(3);
+ delete model.foo;
+ dispatcher.deliver();
+ assertEquals(2, count1);
+ assertEquals(2, count2);
+
+ dispatcher.unobservePropertySet(model, callback2);
+
+ model.unshift(3);
+ delete model.foo;
+ dispatcher.deliver();
+ assertEquals(2, count1);
+ assertEquals(2, count2);
+
+ model.push(6);
+}
+
+</script>
+</html>

0 comments on commit 450d15b

Please sign in to comment.