browser/devtools/shared/observable-object.js

changeset 0
6474c204b198
equal deleted inserted replaced
-1:000000000000 0:a9d9824f00d4
1 /* this source code form is subject to the terms of the mozilla public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4
5 /**
6 * ObservableObject
7 *
8 * An observable object is a JSON-like object that throws
9 * events when its direct properties or properties of any
10 * contained objects, are getting accessed or set.
11 *
12 * Inherits from EventEmitter.
13 *
14 * Properties:
15 * ⬩ object: JSON-like object
16 *
17 * Events:
18 * ⬩ "get" / path (array of property names)
19 * ⬩ "set" / path / new value
20 *
21 * Example:
22 *
23 * let emitter = new ObservableObject({ x: { y: [10] } });
24 * emitter.on("set", console.log);
25 * emitter.on("get", console.log);
26 * let obj = emitter.object;
27 * obj.x.y[0] = 50;
28 *
29 */
30
31 "use strict";
32
33 const EventEmitter = require("devtools/toolkit/event-emitter");
34
35 function ObservableObject(object = {}) {
36 EventEmitter.decorate(this);
37 let handler = new Handler(this);
38 this.object = new Proxy(object, handler);
39 handler._wrappers.set(this.object, object);
40 handler._paths.set(object, []);
41 }
42
43 module.exports = ObservableObject;
44
45 function isObject(x) {
46 if (typeof x === "object")
47 return x !== null;
48 return typeof x === "function";
49 }
50
51 function Handler(emitter) {
52 this._emitter = emitter;
53 this._wrappers = new WeakMap();
54 this._values = new WeakMap();
55 this._paths = new WeakMap();
56 }
57
58 Handler.prototype = {
59 wrap: function(target, key, value) {
60 let path;
61 if (!isObject(value)) {
62 path = this._paths.get(target).concat(key);
63 } else if (this._wrappers.has(value)) {
64 path = this._paths.get(value);
65 } else if (this._paths.has(value)) {
66 path = this._paths.get(value);
67 value = this._values.get(value);
68 } else {
69 path = this._paths.get(target).concat(key);
70 this._paths.set(value, path);
71 let wrapper = new Proxy(value, this);
72 this._wrappers.set(wrapper, value);
73 this._values.set(value, wrapper);
74 value = wrapper;
75 }
76 return [value, path];
77 },
78 unwrap: function(target, key, value) {
79 if (!isObject(value) || !this._wrappers.has(value)) {
80 return [value, this._paths.get(target).concat(key)];
81 }
82 return [this._wrappers.get(value), this._paths.get(target).concat(key)];
83 },
84 get: function(target, key) {
85 let value = target[key];
86 let [wrapped, path] = this.wrap(target, key, value);
87 this._emitter.emit("get", path, value);
88 return wrapped;
89 },
90 set: function(target, key, value) {
91 let [wrapped, path] = this.unwrap(target, key, value);
92 target[key] = value;
93 this._emitter.emit("set", path, value);
94 },
95 getOwnPropertyDescriptor: function(target, key) {
96 let desc = Object.getOwnPropertyDescriptor(target, key);
97 if (desc) {
98 if ("value" in desc) {
99 let [wrapped, path] = this.wrap(target, key, desc.value);
100 desc.value = wrapped;
101 this._emitter.emit("get", path, desc.value);
102 } else {
103 if ("get" in desc) {
104 [desc.get] = this.wrap(target, "get "+key, desc.get);
105 }
106 if ("set" in desc) {
107 [desc.set] = this.wrap(target, "set "+key, desc.set);
108 }
109 }
110 }
111 return desc;
112 },
113 defineProperty: function(target, key, desc) {
114 if ("value" in desc) {
115 let [unwrapped, path] = this.unwrap(target, key, desc.value);
116 desc.value = unwrapped;
117 Object.defineProperty(target, key, desc);
118 this._emitter.emit("set", path, desc.value);
119 } else {
120 if ("get" in desc) {
121 [desc.get] = this.unwrap(target, "get "+key, desc.get);
122 }
123 if ("set" in desc) {
124 [desc.set] = this.unwrap(target, "set "+key, desc.set);
125 }
126 Object.defineProperty(target, key, desc);
127 }
128 }
129 };

mercurial