Bug 1296427 - allow a branch to also be a pref in content implementation; r?gregtatum draft
authorTom Tromey <tom@tromey.com>
Fri, 19 Aug 2016 12:00:12 -0600
changeset 403383 0aa1173b0402004c3f0edea05ad86c4907c0ffa5
parent 403333 8dfc2fdb7ae3990b73f8207aaf62de68a4f47bf5
child 528899 0fc0fb56501722c5a5b2f62a25d1bcb18b0f4832
push id26905
push userbmo:ttromey@mozilla.com
push dateFri, 19 Aug 2016 18:01:07 +0000
reviewersgregtatum
bugs1296427
milestone51.0a1
Bug 1296427 - allow a branch to also be a pref in content implementation; r?gregtatum MozReview-Commit-ID: 2BoIv2eY6DX
devtools/client/shared/shim/Services.js
devtools/client/shared/shim/test/test_service_prefs.html
--- a/devtools/client/shared/shim/Services.js
+++ b/devtools/client/shared/shim/Services.js
@@ -14,151 +14,39 @@ const PREF_STRING = 32;
 const PREF_INT = 64;
 const PREF_BOOL = 128;
 const NS_PREFBRANCH_PREFCHANGE_TOPIC_ID = "nsPref:changed";
 
 // We prefix all our local storage items with this.
 const PREFIX = "Services.prefs:";
 
 /**
- * Create a new preference object.
- *
- * @param {PrefBranch} branch the branch holding this preference
- * @param {String} name the base name of this preference
- * @param {String} fullName the fully-qualified name of this preference
- */
-function Preference(branch, name, fullName) {
-  this.branch = branch;
-  this.name = name;
-  this.fullName = fullName;
-  this.defaultValue = null;
-  this.hasUserValue = false;
-  this.userValue = null;
-  this.type = null;
-}
-
-Preference.prototype = {
-  /**
-   * Return this preference's current value.
-   *
-   * @return {Any} The current value of this preference.  This may
-   *         return a string, a number, or a boolean depending on the
-   *         preference's type.
-   */
-  get: function () {
-    if (this.hasUserValue) {
-      return this.userValue;
-    }
-    return this.defaultValue;
-  },
-
-  /**
-   * Set the preference's value.  The new value is assumed to be a
-   * user value.  After setting the value, this function emits a
-   * change notification.
-   *
-   * @param {Any} value the new value
-   */
-  set: function (value) {
-    if (!this.hasUserValue || value !== this.userValue) {
-      this.userValue = value;
-      this.hasUserValue = true;
-      this.saveAndNotify();
-    }
-  },
-
-  /**
-   * Set the default value for this preference, and emit a
-   * notification if this results in a visible change.
-   *
-   * @param {Any} value the new default value
-   */
-  setDefault: function (value) {
-    if (this.defaultValue !== value) {
-      this.defaultValue = value;
-      if (!this.hasUserValue) {
-        this.saveAndNotify();
-      }
-    }
-  },
-
-  /**
-   * If this preference has a user value, clear it.  If a change was
-   * made, emit a change notification.
-   */
-  clearUserValue: function () {
-    if (this.hasUserValue) {
-      this.userValue = null;
-      this.hasUserValue = false;
-      this.saveAndNotify();
-    }
-  },
-
-  /**
-   * Helper function to write the preference's value to local storage
-   * and then emit a change notification.
-   */
-  saveAndNotify: function () {
-    let store = {
-      type: this.type,
-      defaultValue: this.defaultValue,
-      hasUserValue: this.hasUserValue,
-      userValue: this.userValue,
-    };
-
-    localStorage.setItem(PREFIX + this.fullName, JSON.stringify(store));
-    this.branch._notify(this.name);
-  },
-
-  /**
-   * Change this preference's value without writing it back to local
-   * storage.  This is used to handle changes to local storage that
-   * were made externally.
-   *
-   * @param {Number} type one of the PREF_* values
-   * @param {Any} userValue the user value to use if the pref does not exist
-   * @param {Any} defaultValue the default value to use if the pref
-   *        does not exist
-   * @param {Boolean} hasUserValue if a new pref is created, whether
-   *        the default value is also a user value
-   * @param {Object} store the new value of the preference.  It should
-   *        be of the form {type, defaultValue, hasUserValue, userValue};
-   *        where |type| is one of the PREF_* type constants; |defaultValue|
-   *        and |userValue| are the default and user values, respectively;
-   *        and |hasUserValue| is a boolean indicating whether the user value
-   *        is valid
-   */
-  storageUpdated: function (type, userValue, hasUserValue, defaultValue) {
-    this.type = type;
-    this.defaultValue = defaultValue;
-    this.hasUserValue = hasUserValue;
-    this.userValue = userValue;
-    // There's no need to write this back to local storage, since it
-    // came from there; and this avoids infinite event loops.
-    this.branch._notify(this.name);
-  },
-};
-
-/**
  * Create a new preference branch.  This object conforms largely to
  * nsIPrefBranch and nsIPrefService, though it only implements the
- * subset needed by devtools.
+ * subset needed by devtools.  A preference branch can hold child
+ * preferences while also holding a preference value itself.
  *
  * @param {PrefBranch} parent the parent branch, or null for the root
  *        branch.
  * @param {String} name the base name of this branch
  * @param {String} fullName the fully-qualified name of this branch
  */
 function PrefBranch(parent, name, fullName) {
   this._parent = parent;
   this._name = name;
   this._fullName = fullName;
   this._observers = {};
   this._children = {};
 
+  // Properties used when this branch has a value as well.
+  this._defaultValue = null;
+  this._hasUserValue = false;
+  this._userValue = null;
+  this._type = PREF_INVALID;
+
   if (!parent) {
     this._initializeRoot();
   }
 }
 
 PrefBranch.prototype = {
   PREF_INVALID: PREF_INVALID,
   PREF_STRING: PREF_STRING,
@@ -167,92 +55,92 @@ PrefBranch.prototype = {
 
   /** @see nsIPrefBranch.root.  */
   get root() {
     return this._fullName;
   },
 
   /** @see nsIPrefBranch.getPrefType.  */
   getPrefType: function (prefName) {
-    return this._findPref(prefName).type;
+    return this._findPref(prefName)._type;
   },
 
   /** @see nsIPrefBranch.getBoolPref.  */
   getBoolPref: function (prefName) {
     let thePref = this._findPref(prefName);
-    if (thePref.type !== PREF_BOOL) {
+    if (thePref._type !== PREF_BOOL) {
       throw new Error(`${prefName} does not have bool type`);
     }
-    return thePref.get();
+    return thePref._get();
   },
 
   /** @see nsIPrefBranch.setBoolPref.  */
   setBoolPref: function (prefName, value) {
     if (typeof value !== "boolean") {
       throw new Error("non-bool passed to setBoolPref");
     }
     let thePref = this._findOrCreatePref(prefName, value, true, value);
-    if (thePref.type !== PREF_BOOL) {
+    if (thePref._type !== PREF_BOOL) {
       throw new Error(`${prefName} does not have bool type`);
     }
-    thePref.set(value);
+    thePref._set(value);
   },
 
   /** @see nsIPrefBranch.getCharPref.  */
   getCharPref: function (prefName) {
     let thePref = this._findPref(prefName);
-    if (thePref.type !== PREF_STRING) {
+    if (thePref._type !== PREF_STRING) {
       throw new Error(`${prefName} does not have string type`);
     }
-    return thePref.get();
+    return thePref._get();
   },
 
   /** @see nsIPrefBranch.setCharPref.  */
   setCharPref: function (prefName, value) {
     if (typeof value !== "string") {
       throw new Error("non-string passed to setCharPref");
     }
     let thePref = this._findOrCreatePref(prefName, value, true, value);
-    if (thePref.type !== PREF_STRING) {
+    if (thePref._type !== PREF_STRING) {
       throw new Error(`${prefName} does not have string type`);
     }
-    thePref.set(value);
+    thePref._set(value);
   },
 
   /** @see nsIPrefBranch.getIntPref.  */
   getIntPref: function (prefName) {
     let thePref = this._findPref(prefName);
-    if (thePref.type !== PREF_INT) {
+    if (thePref._type !== PREF_INT) {
       throw new Error(`${prefName} does not have int type`);
     }
-    return thePref.get();
+    return thePref._get();
   },
 
   /** @see nsIPrefBranch.setIntPref.  */
   setIntPref: function (prefName, value) {
     if (typeof value !== "number") {
       throw new Error("non-number passed to setIntPref");
     }
     let thePref = this._findOrCreatePref(prefName, value, true, value);
-    if (thePref.type !== PREF_INT) {
+    if (thePref._type !== PREF_INT) {
       throw new Error(`${prefName} does not have int type`);
     }
-    thePref.set(value);
+    thePref._set(value);
   },
 
   /** @see nsIPrefBranch.clearUserPref */
   clearUserPref: function (prefName) {
     let thePref = this._findPref(prefName);
-    thePref.clearUserValue();
+    thePref._clearUserValue();
   },
 
   /** @see nsIPrefBranch.prefHasUserValue */
   prefHasUserValue: function (prefName) {
     let thePref = this._findPref(prefName);
-    return thePref.hasUserValue;
+    return thePref._hasUserValue;
   },
 
   /** @see nsIPrefBranch.addObserver */
   addObserver: function (domain, observer, holdWeak) {
     if (holdWeak) {
       throw new Error("shim prefs only supports strong observers");
     }
 
@@ -290,16 +178,116 @@ PrefBranch.prototype = {
       prefRoot = prefRoot.slice(0, -1);
     }
     // This is a bit weird since it could erroneously return a pref,
     // not a pref branch.
     return this._findPref(prefRoot);
   },
 
   /**
+   * Return this preference's current value.
+   *
+   * @return {Any} The current value of this preference.  This may
+   *         return a string, a number, or a boolean depending on the
+   *         preference's type.
+   */
+  _get: function () {
+    if (this._hasUserValue) {
+      return this._userValue;
+    }
+    return this._defaultValue;
+  },
+
+  /**
+   * Set the preference's value.  The new value is assumed to be a
+   * user value.  After setting the value, this function emits a
+   * change notification.
+   *
+   * @param {Any} value the new value
+   */
+  _set: function (value) {
+    if (!this._hasUserValue || value !== this._userValue) {
+      this._userValue = value;
+      this._hasUserValue = true;
+      this._saveAndNotify();
+    }
+  },
+
+  /**
+   * Set the default value for this preference, and emit a
+   * notification if this results in a visible change.
+   *
+   * @param {Any} value the new default value
+   */
+  _setDefault: function (value) {
+    if (this._defaultValue !== value) {
+      this._defaultValue = value;
+      if (!this._hasUserValue) {
+        this._saveAndNotify();
+      }
+    }
+  },
+
+  /**
+   * If this preference has a user value, clear it.  If a change was
+   * made, emit a change notification.
+   */
+  _clearUserValue: function () {
+    if (this._hasUserValue) {
+      this._userValue = null;
+      this._hasUserValue = false;
+      this._saveAndNotify();
+    }
+  },
+
+  /**
+   * Helper function to write the preference's value to local storage
+   * and then emit a change notification.
+   */
+  _saveAndNotify: function () {
+    let store = {
+      type: this._type,
+      defaultValue: this._defaultValue,
+      hasUserValue: this._hasUserValue,
+      userValue: this._userValue,
+    };
+
+    localStorage.setItem(PREFIX + this.fullName, JSON.stringify(store));
+    this._parent._notify(this._name);
+  },
+
+  /**
+   * Change this preference's value without writing it back to local
+   * storage.  This is used to handle changes to local storage that
+   * were made externally.
+   *
+   * @param {Number} type one of the PREF_* values
+   * @param {Any} userValue the user value to use if the pref does not exist
+   * @param {Any} defaultValue the default value to use if the pref
+   *        does not exist
+   * @param {Boolean} hasUserValue if a new pref is created, whether
+   *        the default value is also a user value
+   * @param {Object} store the new value of the preference.  It should
+   *        be of the form {type, defaultValue, hasUserValue, userValue};
+   *        where |type| is one of the PREF_* type constants; |defaultValue|
+   *        and |userValue| are the default and user values, respectively;
+   *        and |hasUserValue| is a boolean indicating whether the user value
+   *        is valid
+   */
+  _storageUpdated: function (type, userValue, hasUserValue, defaultValue) {
+    this._type = type;
+    this._defaultValue = defaultValue;
+    this._hasUserValue = hasUserValue;
+    this._userValue = userValue;
+    // There's no need to write this back to local storage, since it
+    // came from there; and this avoids infinite event loops.
+    this._parent._notify(this._name);
+  },
+
+  /**
    * Helper function to find either a Preference or PrefBranch object
    * given its name.  If the name is not found, throws an exception.
    *
    * @param {String} prefName the fully-qualified preference name
    * @return {Object} Either a Preference or PrefBranch object
    */
   _findPref: function (prefName) {
     let branchNames = prefName.split(".");
@@ -373,46 +361,44 @@ PrefBranch.prototype = {
    *        This is also the name of the key in local storage.
    * @param {Any} userValue the user value to use if the pref does not exist
    * @param {Any} defaultValue the default value to use if the pref
    *        does not exist
    * @param {Boolean} hasUserValue if a new pref is created, whether
    *        the default value is also a user value
    */
   _findOrCreatePref: function (keyName, userValue, hasUserValue, defaultValue) {
-    let branchName = keyName.split(".");
-    let prefName = branchName.pop();
-
-    let branch = this._createBranch(branchName);
-    if (!(prefName in branch._children)) {
-      if (hasUserValue && typeof (userValue) !== typeof (defaultValue)) {
-        throw new Error("inconsistent values when creating " + keyName);
-      }
+    let branch = this._createBranch(keyName.split("."));
 
-      let type;
-      switch (typeof (defaultValue)) {
-        case "boolean":
-          type = PREF_BOOL;
-          break;
-        case "number":
-          type = PREF_INT;
-          break;
-        case "string":
-          type = PREF_STRING;
-          break;
-        default:
-          throw new Error("unhandled argument type: " + typeof (defaultValue));
-      }
-
-      let thePref = new Preference(branch, prefName, keyName);
-      thePref.storageUpdated(type, userValue, hasUserValue, defaultValue);
-      branch._children[prefName] = thePref;
+    if (hasUserValue && typeof (userValue) !== typeof (defaultValue)) {
+      throw new Error("inconsistent values when creating " + keyName);
     }
 
-    return branch._children[prefName];
+    let type;
+    switch (typeof (defaultValue)) {
+      case "boolean":
+        type = PREF_BOOL;
+        break;
+      case "number":
+        type = PREF_INT;
+        break;
+      case "string":
+        type = PREF_STRING;
+        break;
+      default:
+        throw new Error("unhandled argument type: " + typeof (defaultValue));
+    }
+
+    if (branch._type === PREF_INVALID) {
+      branch._storageUpdated(type, userValue, hasUserValue, defaultValue);
+    } else if (branch._type !== type) {
+      throw new Error("attempt to change type of pref " + keyName);
+    }
+
+    return branch;
   },
 
   /**
    * Helper function that is called when local storage changes.  This
    * updates the preferences and notifies pref observers as needed.
    *
    * @param {StorageEvent} event the event representing the local
    *        storage change
@@ -427,17 +413,17 @@ PrefBranch.prototype = {
     }
 
     let {type, userValue, hasUserValue, defaultValue} =
         JSON.parse(event.newValue);
     if (event.oldValue === null) {
       this._findOrCreatePref(event.key, userValue, hasUserValue, defaultValue);
     } else {
       let thePref = this._findPref(event.key);
-      thePref.storageUpdated(type, userValue, hasUserValue, defaultValue);
+      thePref._storageUpdated(type, userValue, hasUserValue, defaultValue);
     }
   },
 
   /**
    * Helper function to initialize the root PrefBranch.
    */
   _initializeRoot: function () {
     if (localStorage.length === 0) {
@@ -587,16 +573,16 @@ const Services = {
  * devtools/client/preferences/devtools.js) to install the
  * default preferences.
  *
  * @param {String} name the name of the preference
  * @param {Any} value the default value of the preference
  */
 function pref(name, value) {
   let thePref = Services.prefs._findOrCreatePref(name, value, true, value);
-  thePref.setDefault(value);
+  thePref._setDefault(value);
 }
 
 module.exports = Services;
 // This is exported to silence eslint and, at some point, perhaps to
 // provide it when loading devtools.js in order to install the default
 // preferences.
 exports.pref = pref;
--- a/devtools/client/shared/shim/test/test_service_prefs.html
+++ b/devtools/client/shared/shim/test/test_service_prefs.html
@@ -213,16 +213,19 @@ function do_tests() {
 
   // Make sure we update if the pref change comes from somewhere else.
   clearNotificationList();
   pref("devtools.branch1.someotherstring", "lazuli bunting");
   isDeeply(notifications, {
     "someotherstring": true
   }, "pref worked");
 
+  // Regression test for bug 1296427.
+  pref("devtools.hud.loglimit", 1000);
+  pref("devtools.hud.loglimit.network", 1000);
 
   // Clean up.
   localStorage.clear();
 
   SimpleTest.finish();
 }
 
 SimpleTest.waitForExplicitFinish();