michael@0: /* this source code form is subject to the terms of the mozilla public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: /** michael@0: * ObservableObject michael@0: * michael@0: * An observable object is a JSON-like object that throws michael@0: * events when its direct properties or properties of any michael@0: * contained objects, are getting accessed or set. michael@0: * michael@0: * Inherits from EventEmitter. michael@0: * michael@0: * Properties: michael@0: * ⬩ object: JSON-like object michael@0: * michael@0: * Events: michael@0: * ⬩ "get" / path (array of property names) michael@0: * ⬩ "set" / path / new value michael@0: * michael@0: * Example: michael@0: * michael@0: * let emitter = new ObservableObject({ x: { y: [10] } }); michael@0: * emitter.on("set", console.log); michael@0: * emitter.on("get", console.log); michael@0: * let obj = emitter.object; michael@0: * obj.x.y[0] = 50; michael@0: * michael@0: */ michael@0: michael@0: "use strict"; michael@0: michael@0: const EventEmitter = require("devtools/toolkit/event-emitter"); michael@0: michael@0: function ObservableObject(object = {}) { michael@0: EventEmitter.decorate(this); michael@0: let handler = new Handler(this); michael@0: this.object = new Proxy(object, handler); michael@0: handler._wrappers.set(this.object, object); michael@0: handler._paths.set(object, []); michael@0: } michael@0: michael@0: module.exports = ObservableObject; michael@0: michael@0: function isObject(x) { michael@0: if (typeof x === "object") michael@0: return x !== null; michael@0: return typeof x === "function"; michael@0: } michael@0: michael@0: function Handler(emitter) { michael@0: this._emitter = emitter; michael@0: this._wrappers = new WeakMap(); michael@0: this._values = new WeakMap(); michael@0: this._paths = new WeakMap(); michael@0: } michael@0: michael@0: Handler.prototype = { michael@0: wrap: function(target, key, value) { michael@0: let path; michael@0: if (!isObject(value)) { michael@0: path = this._paths.get(target).concat(key); michael@0: } else if (this._wrappers.has(value)) { michael@0: path = this._paths.get(value); michael@0: } else if (this._paths.has(value)) { michael@0: path = this._paths.get(value); michael@0: value = this._values.get(value); michael@0: } else { michael@0: path = this._paths.get(target).concat(key); michael@0: this._paths.set(value, path); michael@0: let wrapper = new Proxy(value, this); michael@0: this._wrappers.set(wrapper, value); michael@0: this._values.set(value, wrapper); michael@0: value = wrapper; michael@0: } michael@0: return [value, path]; michael@0: }, michael@0: unwrap: function(target, key, value) { michael@0: if (!isObject(value) || !this._wrappers.has(value)) { michael@0: return [value, this._paths.get(target).concat(key)]; michael@0: } michael@0: return [this._wrappers.get(value), this._paths.get(target).concat(key)]; michael@0: }, michael@0: get: function(target, key) { michael@0: let value = target[key]; michael@0: let [wrapped, path] = this.wrap(target, key, value); michael@0: this._emitter.emit("get", path, value); michael@0: return wrapped; michael@0: }, michael@0: set: function(target, key, value) { michael@0: let [wrapped, path] = this.unwrap(target, key, value); michael@0: target[key] = value; michael@0: this._emitter.emit("set", path, value); michael@0: }, michael@0: getOwnPropertyDescriptor: function(target, key) { michael@0: let desc = Object.getOwnPropertyDescriptor(target, key); michael@0: if (desc) { michael@0: if ("value" in desc) { michael@0: let [wrapped, path] = this.wrap(target, key, desc.value); michael@0: desc.value = wrapped; michael@0: this._emitter.emit("get", path, desc.value); michael@0: } else { michael@0: if ("get" in desc) { michael@0: [desc.get] = this.wrap(target, "get "+key, desc.get); michael@0: } michael@0: if ("set" in desc) { michael@0: [desc.set] = this.wrap(target, "set "+key, desc.set); michael@0: } michael@0: } michael@0: } michael@0: return desc; michael@0: }, michael@0: defineProperty: function(target, key, desc) { michael@0: if ("value" in desc) { michael@0: let [unwrapped, path] = this.unwrap(target, key, desc.value); michael@0: desc.value = unwrapped; michael@0: Object.defineProperty(target, key, desc); michael@0: this._emitter.emit("set", path, desc.value); michael@0: } else { michael@0: if ("get" in desc) { michael@0: [desc.get] = this.unwrap(target, "get "+key, desc.get); michael@0: } michael@0: if ("set" in desc) { michael@0: [desc.set] = this.unwrap(target, "set "+key, desc.set); michael@0: } michael@0: Object.defineProperty(target, key, desc); michael@0: } michael@0: } michael@0: };