Bug 898394 - Implement an Object Emitter. r=vporof
authorPaul Rouget <paul@mozilla.com>
Tue, 30 Jul 2013 04:02:00 +0200
changeset 153216 8ead44298a4cd8529ed5e3c45fd040076f8c9a1e
parent 153215 648ab92d0f63fb2543b79ca0896e91048e319362
child 153217 57ee510e991e8e2dddc98cca9ae98b7abcb992e5
push id2859
push userakeybl@mozilla.com
push dateMon, 16 Sep 2013 19:14:59 +0000
treeherdermozilla-beta@87d3c51cd2bf [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;
+}