Bug 898394 - Implement an Object Emitter. r=vporof
authorPaul Rouget <paul@mozilla.com>
Tue, 30 Jul 2013 04:02:00 +0200
changeset 140938 8ead44298a4cd8529ed5e3c45fd040076f8c9a1e
parent 140937 648ab92d0f63fb2543b79ca0896e91048e319362
child 140939 57ee510e991e8e2dddc98cca9ae98b7abcb992e5
push id25043
push userryanvm@gmail.com
push dateFri, 02 Aug 2013 00:24:29 +0000
treeherdermozilla-central@2ba2c2534fdb [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersvporof
bugs898394
milestone25.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 898394 - Implement an Object Emitter. r=vporof
browser/devtools/shared/observable-object.js
browser/devtools/shared/test/Makefile.in
browser/devtools/shared/test/browser_observableobject.js
new file mode 100644
--- /dev/null
+++ b/browser/devtools/shared/observable-object.js
@@ -0,0 +1,120 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * ObservableObject
+ *
+ * An observable object is a JSON-like object that throws
+ * events when its direct properties or properties of any
+ * contained objects, are getting accessed or set.
+ *
+ * Inherits from EventEmitter.
+ *
+ * Properties:
+ * ⬩ object: JSON-like object
+ *
+ * Events:
+ * ⬩ "get" / path (array of property names)
+ * ⬩ "set" / path / new value
+ *
+ * Example:
+ *
+ *   let emitter = new ObservableObject({ x: { y: [10] } });
+ *   emitter.on("set", console.log);
+ *   emitter.on("get", console.log);
+ *   let obj = emitter.object;
+ *   obj.x.y[0] = 50;
+ *
+ */
+
+const EventEmitter = require("devtools/shared/event-emitter");
+
+function ObservableObject(object = {}) {
+  let handler = new Handler(this);
+  this.object = new Proxy(object, handler);
+  handler._wrappers.set(this.object, object);
+  handler._paths.set(object, []);
+}
+
+exports.ObservableObject = ObservableObject;
+
+ObservableObject.prototype = new EventEmitter();
+
+function isObject(value) {
+  return Object(value) === value;
+}
+
+function Handler(emitter) {
+  this._emitter = emitter;
+  this._wrappers = new WeakMap();
+  this._paths = new WeakMap();
+}
+
+Handler.prototype = {
+  wrap: function(target, key, value) {
+    let path;
+    if (!isObject(value)) {
+      path = this._paths.get(target).concat(key);
+    } else if (this._wrappers.has(value)) {
+      path = this._paths.get(value);
+    } else {
+      path = this._paths.get(target).concat(key);
+      this._paths.set(value, path);
+      let wrapper = new Proxy(value, this);
+      this._wrappers.set(wrapper, value);
+      value = wrapper;
+    }
+    return [value, path];
+  },
+  unwrap: function(target, key, value) {
+    if (!isObject(value) || !this._wrappers.has(value)) {
+      return [value, this._paths.get(target).concat(key)];
+    }
+    return [this._wrappers.get(value), this._paths.get(value)];
+  },
+  get: function(target, key) {
+    let value = target[key];
+    let [wrapped, path] = this.wrap(target, key, value);
+    this._emitter.emit("get", path, value);
+    return wrapped;
+  },
+  set: function(target, key, value) {
+    let [wrapped, path] = this.unwrap(target, key, value);
+    target[key] = value;
+    this._emitter.emit("set", path, value);
+  },
+  getOwnPropertyDescriptor: function(target, key) {
+    let desc = Object.getOwnPropertyDescriptor(target, key);
+    if (desc) {
+      if ("value" in desc) {
+        let [wrapped, path] = this.wrap(target, key, desc.value);
+        desc.value = wrapped
+        this._emitter.emit("get", path, desc.value);
+      } else {
+        if ("get" in desc) {
+          [desc.get] = this.wrap(target, "get "+key, desc.get);
+        }
+        if ("set" in desc) {
+          [desc.set] = this.wrap(target, "set "+key, desc.set);
+        }
+      }
+    }
+    return desc;
+  },
+  defineProperty: function(target, key, desc) {
+    if ("value" in desc) {
+      [desc.value, path] = this.unwrap(target, key, desc.value);
+      Object.defineProperty(target, key, desc);
+      this._emitter.emit("set", path, desc.value);
+    } else {
+      if ("get" in desc) {
+        [desc.get] = this.unwrap(target, "get "+key, desc.get);
+      }
+      if ("set" in desc) {
+        [desc.set] = this.unwrap(target, "set "+key, desc.set);
+      }
+      Object.defineProperty(target, key, desc);
+    }
+  }
+};
--- a/browser/devtools/shared/test/Makefile.in
+++ b/browser/devtools/shared/test/Makefile.in
@@ -8,16 +8,17 @@ topsrcdir = @top_srcdir@
 srcdir    = @srcdir@
 VPATH     = @srcdir@
 relativesrcdir  = @relativesrcdir@
 
 include $(DEPTH)/config/autoconf.mk
 
 MOCHITEST_BROWSER_FILES = \
 		browser_eventemitter_basic.js \
+		browser_observableobject.js \
 		browser_layoutHelpers.js \
 		browser_require_basic.js \
 		browser_telemetry_buttonsandsidebar.js \
 		browser_telemetry_toolboxtabs_inspector.js \
 		browser_telemetry_toolboxtabs_jsdebugger.js \
 		browser_telemetry_toolboxtabs_jsprofiler.js \
 		browser_telemetry_toolboxtabs_netmonitor.js \
 		browser_telemetry_toolboxtabs_options.js \
new file mode 100644
--- /dev/null
+++ b/browser/devtools/shared/test/browser_observableobject.js
@@ -0,0 +1,75 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function test() {
+  let tmp = {};
+  Cu.import("resource://gre/modules/devtools/Loader.jsm", tmp);
+  let {ObservableObject} = tmp.devtools.require("devtools/shared/observable-object");
+
+  let rawObject = {};
+  let oe = new ObservableObject(rawObject);
+
+  function str(o) {
+    return JSON.stringify(o);
+  }
+
+  function areObjectsSynced() {
+    is(str(rawObject), str(oe.object), "Objects are synced");
+  }
+
+  areObjectsSynced();
+
+  let index = 0;
+  let expected = [
+    {type: "set", path: "foo", value: 4},
+    {type: "get", path: "foo", value: 4},
+    {type: "get", path: "foo", value: 4},
+    {type: "get", path: "bar", value: undefined},
+    {type: "get", path: "bar", value: undefined},
+    {type: "set", path: "bar", value: {}},
+    {type: "get", path: "bar", value: {}},
+    {type: "get", path: "bar", value: {}},
+    {type: "set", path: "bar.a", value: [1,2,3,4]},
+    {type: "get", path: "bar", value: {a:[1,2,3,4]}},
+    {type: "set", path: "bar.mop", value: 1},
+    {type: "set", path: "bar", value: {}},
+    {type: "set", path: "foo", value: [{a:42}]},
+    {type: "get", path: "foo", value: [{a:42}]},
+    {type: "get", path: "foo.0", value: {a:42}},
+    {type: "get", path: "foo.0.a", value: 42},
+    {type: "get", path: "foo", value: [{a:42}]},
+    {type: "get", path: "foo.0", value: {a:42}},
+    {type: "set", path: "foo.0.a", value: 2},
+  ];
+
+  function callback(event, path, value) {
+    oe.off("get", callback);
+    let e = expected[index];
+    is(event, e.type, "[" + index + "] Right event received");
+    is(path.join("."), e.path, "[" + index + "] Path valid");
+    is(str(value), str(e.value), "[" + index + "] Value valid");
+    index++;
+    areObjectsSynced();
+    oe.on("get", callback);
+    if (index == expected.length) {
+      finish();
+    }
+  }
+
+  oe.on("set", callback);
+  oe.on("get", callback);
+
+  oe.object.foo = 4;
+  oe.object.foo;
+  Object.getOwnPropertyDescriptor(oe.object, "foo")
+  oe.object["bar"];
+  oe.object.bar;
+  oe.object.bar = {};
+  oe.object.bar;
+  oe.object.bar.a = [1,2,3,4];
+  Object.defineProperty(oe.object.bar, "mop", {value:1});
+  oe.object.bar = {};
+  oe.object.foo = [{a:42}];
+  oe.object.foo[0].a;
+  oe.object.foo[0].a = 2;
+}