Bug 903332 - Make watch/unwatch into proxy hooks and such, and make watching/unwatching work on DOM proxies and windows (or at least work as much as it ever did, which is to say kinda-sorta-ish). r=bhackett, r=efaust, a=bajaj
authorJeff Walden <jwalden@mit.edu>
Tue, 29 Oct 2013 16:39:09 -0700
changeset 161330 63d554bec64a
parent 161329 eabe00ce2f40
child 161331 97f50dda2d9a
push id4609
push userjwalden@mit.edu
push date2013-11-18 21:04 +0000
treeherdermozilla-aurora@63d554bec64a [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbhackett, efaust, bajaj
bugs903332
milestone27.0a2
Bug 903332 - Make watch/unwatch into proxy hooks and such, and make watching/unwatching work on DOM proxies and windows (or at least work as much as it ever did, which is to say kinda-sorta-ish). r=bhackett, r=efaust, a=bajaj
content/html/document/test/Makefile.in
content/html/document/test/test_document.watch.html
dom/base/nsGlobalWindow.cpp
dom/bindings/DOMJSProxyHandler.cpp
dom/bindings/DOMJSProxyHandler.h
js/public/Class.h
js/src/builtin/Object.cpp
js/src/builtin/Object.h
js/src/builtin/TypedObject.cpp
js/src/jsfriendapi.h
js/src/jsobj.cpp
js/src/jsobj.h
js/src/jsobjinlines.h
js/src/jsproxy.cpp
js/src/jsproxy.h
js/src/vm/ScopeObject.cpp
js/src/vm/TypedArrayObject.cpp
js/xpconnect/src/XPCWrappedNativeJSOps.cpp
js/xpconnect/src/xpcprivate.h
testing/mochitest/b2g-debug.json
testing/mochitest/b2g-desktop.json
testing/mochitest/b2g.json
--- a/content/html/document/test/Makefile.in
+++ b/content/html/document/test/Makefile.in
@@ -30,16 +30,17 @@ MOCHITEST_FILES = 	test_bug1682.html \
 		test_bug403868.html \
 		test_bug403868.xhtml \
 		$(filter disabled-for-timeouts, test_bug435128.html) \
 		test_bug463104.html \
 		test_form-parsing.html \
 		test_viewport.html \
 		test_documentAll.html \
 		test_document-element-inserted.html \
+		test_document.watch.html \
 		$(filter disabled-temporarily--bug-559932, test_bug445004.html) \
 		bug445004-inner.js \
 		bug445004-outer-rel.html \
 		bug445004-outer-abs.html \
 		bug445004-outer-write.html \
 		bug445004-inner.html \
 		test_bug446483.html \
 		bug446483-iframe.html \
new file mode 100644
--- /dev/null
+++ b/content/html/document/test/test_document.watch.html
@@ -0,0 +1,129 @@
+<!DOCTYPE html>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=903332
+-->
+<head>
+  <meta charset="utf-8">
+  <title>Test for Bug 903332</title>
+  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+  <script type="application/javascript">
+
+  /** Test for Bug 903332 **/
+
+  var watch1Called;
+  function watch1(prop, oldValue, newValue)
+  {
+    is(watch1Called, false, "watch1Called not reset properly?");
+    watch1Called = true;
+
+    is(prop, "cookie", "wrong property name passed to watch1");
+    return newValue;
+  }
+
+  var watch2Called;
+  function watch2(prop, oldValue, newValue)
+  {
+    is(watch2Called, false, "watch2Called not reset properly?");
+    watch2Called = true;
+
+    is(prop, "cookie", "wrong property name passed to watch2");
+    return newValue;
+  }
+
+  // Just in case subsequent tests depend on a particular value...
+  var originalValue = document.cookie;
+  ok(true, "originalValue: " + originalValue);
+
+  var originalPrefix = originalValue.length > 0 ? originalValue + "; " : "";
+
+  try
+  {
+    // trial set (no watch) to verify things work
+    document.cookie = "first=set";
+    is(document.cookie, originalPrefix + "first=set",
+       "first value correct");
+
+    // add a watch
+    document.watch("cookie", watch1);
+
+    // set, check for watch invoked
+    watch1Called = false;
+    document.cookie = "second=set";
+    is(watch1Called, true, "watch1 function should be called");
+    is(document.cookie, originalPrefix + "first=set; second=set",
+       "second value correct");
+
+    // and a second time, just in case
+    watch1Called = false;
+    document.cookie = "third=set";
+    is(watch1Called, true, "watch1 function should be called");
+    is(document.cookie, originalPrefix + "first=set; second=set; third=set",
+       "third value correct");
+
+    // overwrite the current watch with a new one
+    document.watch("cookie", watch2);
+
+    // set, check for watch invoked
+    watch1Called = false;
+    watch2Called = false;
+    document.cookie = "fourth=set";
+    is(watch1Called, false, "watch1 invoked erroneously");
+    is(watch2Called, true, "watch2 function should be called");
+    is(document.cookie, originalPrefix + "first=set; second=set; third=set; fourth=set",
+       "fourth value correct");
+
+    // and a second time, just in case
+    watch1Called = false;
+    watch2Called = false;
+    document.cookie = "fifth=set";
+    is(watch1Called, false, "watch1 invoked erroneously");
+    is(watch2Called, true, "watch2 function should be called");
+    is(document.cookie, originalPrefix + "first=set; second=set; third=set; fourth=set; fifth=set",
+       "fifth value correct");
+
+    // remove the watch
+    document.unwatch("cookie");
+
+    // check for non-invocation now
+    watch1Called = false;
+    watch2Called = false;
+    document.cookie = "sixth=set";
+    is(watch1Called, false, "watch1 shouldn't be called");
+    is(watch2Called, false, "watch2 shouldn't be called");
+    is(document.cookie, originalPrefix + "first=set; second=set; third=set; fourth=set; fifth=set; sixth=set",
+       "sixth value correct");
+  }
+  finally
+  {
+    // reset
+    document.unwatch("cookie"); // harmless, should be no-op except if bugs
+
+    var d = new Date();
+    d.setTime(0);
+    var suffix = "=; expires=" + d.toGMTString();
+
+    document.cookie = "first" + suffix;
+    document.cookie = "second" + suffix;
+    document.cookie = "third" + suffix;
+    document.cookie = "fourth" + suffix;
+    document.cookie = "fifth" + suffix;
+    document.cookie = "sixth" + suffix;
+  }
+
+  is(document.cookie, originalValue,
+     "document.cookie isn't what it was initially!  expect bustage further " +
+     "down the line");
+  </script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=903332">Mozilla Bug 903332</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+</body>
+</html>
--- a/dom/base/nsGlobalWindow.cpp
+++ b/dom/base/nsGlobalWindow.cpp
@@ -609,16 +609,21 @@ public:
                                    JS::Handle<JSObject*> proxy,
                                    JS::AutoIdVector &props) MOZ_OVERRIDE;
   virtual bool delete_(JSContext *cx, JS::Handle<JSObject*> proxy,
                        JS::Handle<jsid> id,
                        bool *bp) MOZ_OVERRIDE;
   virtual bool enumerate(JSContext *cx, JS::Handle<JSObject*> proxy,
                          JS::AutoIdVector &props) MOZ_OVERRIDE;
 
+  virtual bool watch(JSContext *cx, JS::Handle<JSObject*> proxy,
+                     JS::Handle<jsid> id, JS::Handle<JSObject*> callable) MOZ_OVERRIDE;
+  virtual bool unwatch(JSContext *cx, JS::Handle<JSObject*> proxy,
+                       JS::Handle<jsid> id) MOZ_OVERRIDE;
+
   // Derived traps
   virtual bool has(JSContext *cx, JS::Handle<JSObject*> proxy,
                    JS::Handle<jsid> id, bool *bp) MOZ_OVERRIDE;
   virtual bool hasOwn(JSContext *cx, JS::Handle<JSObject*> proxy,
                       JS::Handle<jsid> id, bool *bp) MOZ_OVERRIDE;
   virtual bool get(JSContext *cx, JS::Handle<JSObject*> proxy,
                    JS::Handle<JSObject*> receiver,
                    JS::Handle<jsid> id,
@@ -958,16 +963,30 @@ nsOuterWindowProxy::AppendIndexedPropert
   }
   for (int32_t i = 0; i < int32_t(length); ++i) {
     props.append(INT_TO_JSID(i));
   }
 
   return true;
 }
 
+bool
+nsOuterWindowProxy::watch(JSContext *cx, JS::Handle<JSObject*> proxy,
+                          JS::Handle<jsid> id, JS::Handle<JSObject*> callable)
+{
+  return js::WatchGuts(cx, proxy, id, callable);
+}
+
+bool
+nsOuterWindowProxy::unwatch(JSContext *cx, JS::Handle<JSObject*> proxy,
+                            JS::Handle<jsid> id)
+{
+  return js::UnwatchGuts(cx, proxy, id);
+}
+
 nsOuterWindowProxy
 nsOuterWindowProxy::singleton;
 
 class nsChromeOuterWindowProxy : public nsOuterWindowProxy
 {
 public:
   nsChromeOuterWindowProxy() : nsOuterWindowProxy() {}
 
--- a/dom/bindings/DOMJSProxyHandler.cpp
+++ b/dom/bindings/DOMJSProxyHandler.cpp
@@ -230,16 +230,29 @@ BaseDOMProxyHandler::enumerate(JSContext
   if (!JS_GetPrototype(cx, proxy, &proto))  {
     return false;
   }
   return getOwnPropertyNames(cx, proxy, props) &&
          (!proto || js::GetPropertyNames(cx, proto, 0, &props));
 }
 
 bool
+BaseDOMProxyHandler::watch(JSContext* cx, JS::Handle<JSObject*> proxy, JS::Handle<jsid> id,
+                           JS::Handle<JSObject*> callable)
+{
+  return js::WatchGuts(cx, proxy, id, callable);
+}
+
+bool
+BaseDOMProxyHandler::unwatch(JSContext* cx, JS::Handle<JSObject*> proxy, JS::Handle<jsid> id)
+{
+  return js::UnwatchGuts(cx, proxy, id);
+}
+
+bool
 DOMProxyHandler::has(JSContext* cx, JS::Handle<JSObject*> proxy, JS::Handle<jsid> id, bool* bp)
 {
   if (!hasOwn(cx, proxy, id, bp)) {
     return false;
   }
 
   if (*bp) {
     // We have the property ourselves; no need to worry about our prototype
--- a/dom/bindings/DOMJSProxyHandler.h
+++ b/dom/bindings/DOMJSProxyHandler.h
@@ -53,16 +53,21 @@ public:
   // Implementations of traps that can be implemented in terms of
   // fundamental traps.
   bool enumerate(JSContext* cx, JS::Handle<JSObject*> proxy,
                  JS::AutoIdVector& props) MOZ_OVERRIDE;
   bool getPropertyDescriptor(JSContext* cx, JS::Handle<JSObject*> proxy,
                              JS::Handle<jsid> id,
                              JS::MutableHandle<JSPropertyDescriptor> desc,
                              unsigned flags) MOZ_OVERRIDE;
+
+  bool watch(JSContext* cx, JS::Handle<JSObject*> proxy, JS::Handle<jsid> id,
+             JS::Handle<JSObject*> callable) MOZ_OVERRIDE;
+  bool unwatch(JSContext* cx, JS::Handle<JSObject*> proxy,
+               JS::Handle<jsid> id) MOZ_OVERRIDE;
 };
 
 class DOMProxyHandler : public BaseDOMProxyHandler
 {
 public:
   DOMProxyHandler(const DOMClass& aClass)
     : BaseDOMProxyHandler(ProxyFamily()),
       mClass(aClass)
--- a/js/public/Class.h
+++ b/js/public/Class.h
@@ -372,16 +372,21 @@ typedef bool
 typedef bool
 (* DeletePropertyOp)(JSContext *cx, JS::HandleObject obj, JS::Handle<PropertyName*> name,
                      bool *succeeded);
 typedef bool
 (* DeleteElementOp)(JSContext *cx, JS::HandleObject obj, uint32_t index, bool *succeeded);
 typedef bool
 (* DeleteSpecialOp)(JSContext *cx, JS::HandleObject obj, HandleSpecialId sid, bool *succeeded);
 
+typedef bool
+(* WatchOp)(JSContext *cx, JS::HandleObject obj, JS::HandleId id, JS::HandleObject callable);
+
+typedef bool
+(* UnwatchOp)(JSContext *cx, JS::HandleObject obj, JS::HandleId id);
 
 typedef JSObject *
 (* ObjectOp)(JSContext *cx, JS::HandleObject obj);
 typedef void
 (* FinalizeOp)(FreeOp *fop, JSObject *obj);
 
 #define JS_CLASS_MEMBERS                                                      \
     const char          *name;                                                \
@@ -460,25 +465,27 @@ struct ObjectOps
     StrictPropertyIdOp  setProperty;
     StrictElementIdOp   setElement;
     StrictSpecialIdOp   setSpecial;
     GenericAttributesOp getGenericAttributes;
     GenericAttributesOp setGenericAttributes;
     DeletePropertyOp    deleteProperty;
     DeleteElementOp     deleteElement;
     DeleteSpecialOp     deleteSpecial;
+    WatchOp             watch;
+    UnwatchOp           unwatch;
 
     JSNewEnumerateOp    enumerate;
     ObjectOp            thisObject;
 };
 
 #define JS_NULL_OBJECT_OPS                                                    \
     {nullptr,nullptr,nullptr,nullptr,nullptr,nullptr,nullptr,nullptr,nullptr, \
      nullptr,nullptr,nullptr,nullptr,nullptr,nullptr,nullptr,nullptr,nullptr, \
-     nullptr,nullptr,nullptr,nullptr,nullptr,nullptr}
+     nullptr,nullptr,nullptr,nullptr,nullptr,nullptr,nullptr,nullptr}
 
 } // namespace js
 
 // Classes, objects, and properties.
 
 typedef void (*JSClassInternal)();
 
 struct JSClass {
@@ -497,17 +504,17 @@ struct JSClass {
     // Optional members (may be null).
     JSFinalizeOp        finalize;
     JSCheckAccessOp     checkAccess;
     JSNative            call;
     JSHasInstanceOp     hasInstance;
     JSNative            construct;
     JSTraceOp           trace;
 
-    void                *reserved[40];
+    void                *reserved[42];
 };
 
 #define JSCLASS_HAS_PRIVATE             (1<<0)  // objects have private slot
 #define JSCLASS_NEW_ENUMERATE           (1<<1)  // has JSNewEnumerateOp hook
 #define JSCLASS_NEW_RESOLVE             (1<<2)  // has JSNewResolveOp hook
 #define JSCLASS_PRIVATE_IS_NSISUPPORTS  (1<<3)  // private is (nsISupports *)
 #define JSCLASS_IS_DOMJSCLASS           (1<<4)  // objects are DOM
 #define JSCLASS_IMPLEMENTS_BARRIERS     (1<<5)  // Correctly implements GC read
--- a/js/src/builtin/Object.cpp
+++ b/js/src/builtin/Object.cpp
@@ -520,19 +520,19 @@ obj_getPrototypeOf(JSContext *cx, unsign
     if (!Invoke(cx, args2))
         return false;
     args.rval().set(args2.rval());
     return true;
 }
 
 #if JS_HAS_OBJ_WATCHPOINT
 
-static bool
-obj_watch_handler(JSContext *cx, JSObject *obj_, jsid id_, jsval old,
-                  jsval *nvp, void *closure)
+bool
+js::WatchHandler(JSContext *cx, JSObject *obj_, jsid id_, JS::Value old,
+                 JS::Value *nvp, void *closure)
 {
     RootedObject obj(cx, obj_);
     RootedId id(cx, id_);
 
     /* Avoid recursion on (obj, id) already being watched on cx. */
     AutoResolving resolving(cx, obj, id, AutoResolving::WATCH);
     if (resolving.alreadyStarted())
         return true;
@@ -569,19 +569,21 @@ obj_watch(JSContext *cx, unsigned argc, 
     if (!obj)
         return false;
 
     RootedValue tmp(cx);
     unsigned attrs;
     if (!CheckAccess(cx, obj, propid, JSACC_WATCH, &tmp, &attrs))
         return false;
 
-    args.rval().setUndefined();
+    if (!JSObject::watch(cx, obj, propid, callable))
+        return false;
 
-    return JS_SetWatchPoint(cx, obj, propid, obj_watch_handler, callable);
+    args.rval().setUndefined();
+    return true;
 }
 
 static bool
 obj_unwatch(JSContext *cx, unsigned argc, Value *vp)
 {
     CallArgs args = CallArgsFromVp(argc, vp);
 
     RootedObject obj(cx, ToObject(cx, args.thisv()));
@@ -590,17 +592,22 @@ obj_unwatch(JSContext *cx, unsigned argc
     args.rval().setUndefined();
     RootedId id(cx);
     if (argc != 0) {
         if (!ValueToId<CanGC>(cx, args[0], &id))
             return false;
     } else {
         id = JSID_VOID;
     }
-    return JS_ClearWatchPoint(cx, obj, id, nullptr, nullptr);
+
+    if (!JSObject::unwatch(cx, obj, id))
+        return false;
+
+    args.rval().setUndefined();
+    return true;
 }
 
 #endif /* JS_HAS_OBJ_WATCHPOINT */
 
 /* ECMA 15.2.4.5. */
 static bool
 obj_hasOwnProperty(JSContext *cx, unsigned argc, Value *vp)
 {
--- a/js/src/builtin/Object.h
+++ b/js/src/builtin/Object.h
@@ -3,28 +3,33 @@
  * 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/. */
 
 #ifndef builtin_Object_h
 #define builtin_Object_h
 
 #include "jsapi.h"
-#include "js/Value.h"
+
+namespace JS { class Value; }
 
 namespace js {
 
 extern const JSFunctionSpec object_methods[];
 extern const JSFunctionSpec object_static_methods[];
 
 // Object constructor native. Exposed only so the JIT can know its address.
 bool
 obj_construct(JSContext *cx, unsigned argc, JS::Value *vp);
 
 #if JS_HAS_TOSOURCE
 // Object.prototype.toSource. Function.prototype.toSource and uneval use this.
 JSString *
 ObjectToSource(JSContext *cx, JS::HandleObject obj);
 #endif // JS_HAS_TOSOURCE
 
+extern bool
+WatchHandler(JSContext *cx, JSObject *obj, jsid id, JS::Value old,
+             JS::Value *nvp, void *closure);
+
 } /* namespace js */
 
 #endif /* builtin_Object_h */
--- a/js/src/builtin/TypedObject.cpp
+++ b/js/src/builtin/TypedObject.cpp
@@ -2198,16 +2198,17 @@ const Class TypedObject::class_ = {
         TypedDatum::obj_setProperty,
         TypedDatum::obj_setElement,
         TypedDatum::obj_setSpecial,
         TypedDatum::obj_getGenericAttributes,
         TypedDatum::obj_setGenericAttributes,
         TypedDatum::obj_deleteProperty,
         TypedDatum::obj_deleteElement,
         TypedDatum::obj_deleteSpecial,
+        nullptr, nullptr, // watch/unwatch
         TypedDatum::obj_enumerate,
         NULL, /* thisObject */
     }
 };
 
 /*static*/ JSObject *
 TypedObject::createZeroed(JSContext *cx, HandleObject type)
 {
@@ -2288,16 +2289,17 @@ const Class TypedHandle::class_ = {
         TypedDatum::obj_setProperty,
         TypedDatum::obj_setElement,
         TypedDatum::obj_setSpecial,
         TypedDatum::obj_getGenericAttributes,
         TypedDatum::obj_setGenericAttributes,
         TypedDatum::obj_deleteProperty,
         TypedDatum::obj_deleteElement,
         TypedDatum::obj_deleteSpecial,
+        nullptr, nullptr, // watch/unwatch
         TypedDatum::obj_enumerate,
         NULL, /* thisObject */
     }
 };
 
 const JSFunctionSpec TypedHandle::handleStaticMethods[] = {
     {"move", {NULL, NULL}, 3, 0, "HandleMove"},
     {"get", {NULL, NULL}, 1, 0, "HandleGet"},
--- a/js/src/jsfriendapi.h
+++ b/js/src/jsfriendapi.h
@@ -1304,16 +1304,43 @@ JS_GetDataViewByteLength(JSObject *obj);
  * |obj| must have passed a JS_IsDataViewObject test, or somehow be known that
  * it would pass such a test: it is a data view or a wrapper of a data view,
  * and the unwrapping will succeed. If cx is nullptr, then DEBUG builds may be
  * unable to assert when unwrapping should be disallowed.
  */
 JS_FRIEND_API(void *)
 JS_GetDataViewData(JSObject *obj);
 
+namespace js {
+
+/*
+ * Add a watchpoint -- in the Object.prototype.watch sense -- to |obj| for the
+ * property |id|, using the callable object |callable| as the function to be
+ * called for notifications.
+ *
+ * This is an internal function exposed -- temporarily -- only so that DOM
+ * proxies can be watchable.  Don't use it!  We'll soon kill off the
+ * Object.prototype.{,un}watch functions, at which point this will go too.
+ */
+extern JS_FRIEND_API(bool)
+WatchGuts(JSContext *cx, JS::HandleObject obj, JS::HandleId id, JS::HandleObject callable);
+
+/*
+ * Remove a watchpoint -- in the Object.prototype.watch sense -- from |obj| for
+ * the property |id|.
+ *
+ * This is an internal function exposed -- temporarily -- only so that DOM
+ * proxies can be watchable.  Don't use it!  We'll soon kill off the
+ * Object.prototype.{,un}watch functions, at which point this will go too.
+ */
+extern JS_FRIEND_API(bool)
+UnwatchGuts(JSContext *cx, JS::HandleObject obj, JS::HandleId id);
+
+} // namespace js
+
 /*
  * A class, expected to be passed by value, which represents the CallArgs for a
  * JSJitGetterOp.
  */
 class JSJitGetterCallArgs : protected JS::MutableHandleValue
 {
   public:
     explicit JSJitGetterCallArgs(const JS::CallArgs& args)
--- a/js/src/jsobj.cpp
+++ b/js/src/jsobj.cpp
@@ -16,30 +16,32 @@
 #include "mozilla/Util.h"
 
 #include <string.h>
 
 #include "jsapi.h"
 #include "jsarray.h"
 #include "jsatom.h"
 #include "jscntxt.h"
+#include "jsfriendapi.h"
 #include "jsfun.h"
 #include "jsgc.h"
 #include "jsiter.h"
 #include "jsnum.h"
 #include "jsopcode.h"
 #include "jsprf.h"
 #include "jsproxy.h"
 #include "jsscript.h"
 #include "jsstr.h"
 #include "jstypes.h"
 #include "jsutil.h"
 #include "jswatchpoint.h"
 #include "jswrapper.h"
 
+#include "builtin/Object.h"
 #include "frontend/BytecodeCompiler.h"
 #include "gc/Marking.h"
 #include "jit/AsmJSModule.h"
 #include "jit/BaselineJIT.h"
 #include "js/MemoryMetrics.h"
 #include "js/OldDebugAPI.h"
 #include "vm/ArgumentsObject.h"
 #include "vm/Interpreter.h"
@@ -5006,16 +5008,80 @@ baseops::DeleteElement(JSContext *cx, Ha
 bool
 baseops::DeleteSpecial(JSContext *cx, HandleObject obj, HandleSpecialId sid, bool *succeeded)
 {
     Rooted<jsid> id(cx, SPECIALID_TO_JSID(sid));
     return baseops::DeleteGeneric(cx, obj, id, succeeded);
 }
 
 bool
+js::WatchGuts(JSContext *cx, JS::HandleObject origObj, JS::HandleId id, JS::HandleObject callable)
+{
+    RootedObject obj(cx, GetInnerObject(cx, origObj));
+    if (origObj != obj) {
+        // If by unwrapping and innerizing, we changed the object, check again
+        // to make sure that we're allowed to set a watch point.
+        RootedValue v(cx);
+        unsigned attrs;
+        if (!CheckAccess(cx, obj, id, JSACC_WATCH, &v, &attrs))
+            return false;
+    }
+
+    if (obj->isNative()) {
+        // Use sparse indexes for watched objects, as dense elements can be
+        // written to without checking the watchpoint map.
+        if (!JSObject::sparsifyDenseElements(cx, obj))
+            return false;
+
+        types::MarkTypePropertyConfigured(cx, obj, id);
+    }
+
+    WatchpointMap *wpmap = cx->compartment()->watchpointMap;
+    if (!wpmap) {
+        wpmap = cx->runtime()->new_<WatchpointMap>();
+        if (!wpmap || !wpmap->init()) {
+            js_ReportOutOfMemory(cx);
+            return false;
+        }
+        cx->compartment()->watchpointMap = wpmap;
+    }
+
+    return wpmap->watch(cx, obj, id, js::WatchHandler, callable);
+}
+
+bool
+baseops::Watch(JSContext *cx, JS::HandleObject obj, JS::HandleId id, JS::HandleObject callable)
+{
+    if (!obj->isNative()) {
+        JS_ReportErrorNumber(cx, js_GetErrorMessage, nullptr, JSMSG_CANT_WATCH,
+                             obj->getClass()->name);
+        return false;
+    }
+
+    return WatchGuts(cx, obj, id, callable);
+}
+
+bool
+js::UnwatchGuts(JSContext *cx, JS::HandleObject origObj, JS::HandleId id)
+{
+    // Looking in the map for an unsupported object will never hit, so we don't
+    // need to check for nativeness or watchable-ness here.
+    RootedObject obj(cx, GetInnerObject(cx, origObj));
+    if (WatchpointMap *wpmap = cx->compartment()->watchpointMap)
+        wpmap->unwatch(obj, id, nullptr, nullptr);
+    return true;
+}
+
+bool
+baseops::Unwatch(JSContext *cx, JS::HandleObject obj, JS::HandleId id)
+{
+    return UnwatchGuts(cx, obj, id);
+}
+
+bool
 js::HasDataProperty(JSContext *cx, JSObject *obj, jsid id, Value *vp)
 {
     if (JSID_IS_INT(id) && obj->containsDenseElement(JSID_TO_INT(id))) {
         *vp = obj->getDenseElement(JSID_TO_INT(id));
         return true;
     }
 
     if (Shape *shape = obj->nativeLookup(cx, id)) {
--- a/js/src/jsobj.h
+++ b/js/src/jsobj.h
@@ -150,16 +150,22 @@ extern bool
 DeleteElement(JSContext *cx, HandleObject obj, uint32_t index, bool *succeeded);
 
 extern bool
 DeleteSpecial(JSContext *cx, HandleObject obj, HandleSpecialId sid, bool *succeeded);
 
 extern bool
 DeleteGeneric(JSContext *cx, HandleObject obj, HandleId id, bool *succeeded);
 
+extern bool
+Watch(JSContext *cx, JS::HandleObject obj, JS::HandleId id, JS::HandleObject callable);
+
+extern bool
+Unwatch(JSContext *cx, JS::HandleObject obj, JS::HandleId id);
+
 } /* namespace js::baseops */
 
 extern const Class IntlClass;
 extern const Class JSONClass;
 extern const Class MathClass;
 
 class ArrayBufferObject;
 class GlobalObject;
@@ -1088,16 +1094,20 @@ class JSObject : public js::ObjectImpl
                                       bool *succeeded);
     static inline bool deleteElement(JSContext *cx, js::HandleObject obj,
                                      uint32_t index, bool *succeeded);
     static inline bool deleteSpecial(JSContext *cx, js::HandleObject obj,
                                      js::HandleSpecialId sid, bool *succeeded);
     static bool deleteByValue(JSContext *cx, js::HandleObject obj,
                               const js::Value &property, bool *succeeded);
 
+    static inline bool watch(JSContext *cx, JS::HandleObject obj, JS::HandleId id,
+                             JS::HandleObject callable);
+    static inline bool unwatch(JSContext *cx, JS::HandleObject obj, JS::HandleId id);
+
     static bool enumerate(JSContext *cx, JS::HandleObject obj, JSIterateOp iterop,
                           JS::MutableHandleValue statep, JS::MutableHandleId idp)
     {
         JSNewEnumerateOp op = obj->getOps()->enumerate;
         return (op ? op : JS_EnumerateState)(cx, obj, iterop, statep, idp);
     }
 
     static bool defaultValue(JSContext *cx, js::HandleObject obj, JSType hint,
--- a/js/src/jsobjinlines.h
+++ b/js/src/jsobjinlines.h
@@ -67,16 +67,31 @@ JSObject::deleteSpecial(JSContext *cx, j
 {
     JS::RootedId id(cx, SPECIALID_TO_JSID(sid));
     js::types::AddTypePropertyId(cx, obj, id, js::types::Type::UndefinedType());
     js::types::MarkTypePropertyConfigured(cx, obj, id);
     js::DeleteSpecialOp op = obj->getOps()->deleteSpecial;
     return (op ? op : js::baseops::DeleteSpecial)(cx, obj, sid, succeeded);
 }
 
+/* static */ inline bool
+JSObject::watch(JSContext *cx, JS::HandleObject obj, JS::HandleId id,
+                JS::HandleObject callable)
+{
+    js::WatchOp op = obj->getOps()->watch;
+    return (op ? op : js::baseops::Watch)(cx, obj, id, callable);
+}
+
+/* static */ inline bool
+JSObject::unwatch(JSContext *cx, JS::HandleObject obj, JS::HandleId id)
+{
+    js::UnwatchOp op = obj->getOps()->unwatch;
+    return (op ? op : js::baseops::Unwatch)(cx, obj, id);
+}
+
 inline void
 JSObject::finalize(js::FreeOp *fop)
 {
     js::probes::FinalizeObject(this);
 
 #ifdef DEBUG
     JS_ASSERT(isTenured());
     if (!IsBackgroundFinalized(tenuredGetAllocKind())) {
--- a/js/src/jsproxy.cpp
+++ b/js/src/jsproxy.cpp
@@ -366,16 +366,29 @@ BaseProxyHandler::weakmapKeyDelegate(JSO
 bool
 BaseProxyHandler::getPrototypeOf(JSContext *cx, HandleObject proxy, MutableHandleObject protop)
 {
     // The default implementation here just uses proto of the proxy object.
     protop.set(proxy->getTaggedProto().toObjectOrNull());
     return true;
 }
 
+bool
+BaseProxyHandler::watch(JSContext *cx, HandleObject proxy, HandleId id, HandleObject callable)
+{
+    JS_ReportErrorNumber(cx, js_GetErrorMessage, nullptr, JSMSG_CANT_WATCH,
+                         proxy->getClass()->name);
+    return false;
+}
+
+bool
+BaseProxyHandler::unwatch(JSContext *cx, HandleObject proxy, HandleId id)
+{
+    return true;
+}
 
 bool
 DirectProxyHandler::getPropertyDescriptor(JSContext *cx, HandleObject proxy, HandleId id,
                                           MutableHandle<PropertyDescriptor> desc, unsigned flags)
 {
     assertEnteredPolicy(cx, proxy, id);
     JS_ASSERT(!hasPrototype()); // Should never be called if there's a prototype.
     RootedObject target(cx, proxy->as<ProxyObject>().target());
@@ -2742,16 +2755,30 @@ bool
 Proxy::getPrototypeOf(JSContext *cx, HandleObject proxy, MutableHandleObject proto)
 {
     JS_CHECK_RECURSION(cx, return false);
     return proxy->as<ProxyObject>().handler()->getPrototypeOf(cx, proxy, proto);
 }
 
 JSObject * const Proxy::LazyProto = reinterpret_cast<JSObject *>(0x1);
 
+/* static */ bool
+Proxy::watch(JSContext *cx, JS::HandleObject proxy, JS::HandleId id, JS::HandleObject callable)
+{
+    JS_CHECK_RECURSION(cx, return false);
+    return proxy->as<ProxyObject>().handler()->watch(cx, proxy, id, callable);
+}
+
+/* static */ bool
+Proxy::unwatch(JSContext *cx, JS::HandleObject proxy, JS::HandleId id)
+{
+    JS_CHECK_RECURSION(cx, return false);
+    return proxy->as<ProxyObject>().handler()->unwatch(cx, proxy, id);
+}
+
 static JSObject *
 proxy_innerObject(JSContext *cx, HandleObject obj)
 {
     return obj->as<ProxyObject>().private_().toObjectOrNull();
 }
 
 static bool
 proxy_LookupGeneric(JSContext *cx, HandleObject obj, HandleId id,
@@ -3041,16 +3068,28 @@ static bool
 proxy_Construct(JSContext *cx, unsigned argc, Value *vp)
 {
     CallArgs args = CallArgsFromVp(argc, vp);
     RootedObject proxy(cx, &args.callee());
     JS_ASSERT(proxy->is<ProxyObject>());
     return Proxy::construct(cx, proxy, args);
 }
 
+static bool
+proxy_Watch(JSContext *cx, JS::HandleObject obj, JS::HandleId id, JS::HandleObject callable)
+{
+    return Proxy::watch(cx, obj, id, callable);
+}
+
+static bool
+proxy_Unwatch(JSContext *cx, JS::HandleObject obj, JS::HandleId id)
+{
+    return Proxy::unwatch(cx, obj, id);
+}
+
 #define PROXY_CLASS_EXT                             \
     {                                               \
         nullptr,             /* outerObject */      \
         nullptr,             /* innerObject */      \
         nullptr,             /* iteratorObject */   \
         false,               /* isWrappedNative */  \
         proxy_WeakmapKeyDelegate                    \
     }
@@ -3093,16 +3132,17 @@ proxy_Construct(JSContext *cx, unsigned 
         proxy_SetProperty,                          \
         proxy_SetElement,                           \
         proxy_SetSpecial,                           \
         proxy_GetGenericAttributes,                 \
         proxy_SetGenericAttributes,                 \
         proxy_DeleteProperty,                       \
         proxy_DeleteElement,                        \
         proxy_DeleteSpecial,                        \
+        proxy_Watch, proxy_Unwatch,                 \
         nullptr,             /* enumerate       */  \
         nullptr,             /* thisObject      */  \
     }                                               \
 }
 
 const Class js::ProxyObject::uncallableClass_ = PROXY_CLASS(nullptr, nullptr);
 const Class js::ProxyObject::callableClass_ = PROXY_CLASS(proxy_Call, proxy_Construct);
 
@@ -3150,16 +3190,17 @@ const Class js::OuterWindowProxyObject::
         proxy_SetProperty,
         proxy_SetElement,
         proxy_SetSpecial,
         proxy_GetGenericAttributes,
         proxy_SetGenericAttributes,
         proxy_DeleteProperty,
         proxy_DeleteElement,
         proxy_DeleteSpecial,
+        proxy_Watch, proxy_Unwatch,
         nullptr,             /* enumerate       */
         nullptr,             /* thisObject      */
     }
 };
 
 const Class* const js::OuterWindowProxyClassPtr = &OuterWindowProxyObject::class_;
 
 JS_FRIEND_API(JSObject *)
--- a/js/src/jsproxy.h
+++ b/js/src/jsproxy.h
@@ -161,16 +161,22 @@ class JS_FRIEND_API(BaseProxyHandler)
     virtual JSString *fun_toString(JSContext *cx, HandleObject proxy, unsigned indent);
     virtual bool regexp_toShared(JSContext *cx, HandleObject proxy, RegExpGuard *g);
     virtual bool defaultValue(JSContext *cx, HandleObject obj, JSType hint, MutableHandleValue vp);
     virtual void finalize(JSFreeOp *fop, JSObject *proxy);
     virtual bool getElementIfPresent(JSContext *cx, HandleObject obj, HandleObject receiver,
                                      uint32_t index, MutableHandleValue vp, bool *present);
     virtual bool getPrototypeOf(JSContext *cx, HandleObject proxy, MutableHandleObject protop);
 
+    // These two hooks must be overridden, or not overridden, in tandem -- no
+    // overriding just one!
+    virtual bool watch(JSContext *cx, JS::HandleObject proxy, JS::HandleId id,
+                       JS::HandleObject callable);
+    virtual bool unwatch(JSContext *cx, JS::HandleObject proxy, JS::HandleId id);
+
     /* See comment for weakmapKeyDelegateOp in js/Class.h. */
     virtual JSObject *weakmapKeyDelegate(JSObject *proxy);
 };
 
 /*
  * DirectProxyHandler includes a notion of a target object. All traps are
  * reimplemented such that they forward their behavior to the target. This
  * allows consumers of this class to forward to another object as transparently
@@ -270,16 +276,20 @@ class Proxy
     static bool hasInstance(JSContext *cx, HandleObject proxy, MutableHandleValue v, bool *bp);
     static bool objectClassIs(HandleObject obj, ESClassValue classValue, JSContext *cx);
     static const char *className(JSContext *cx, HandleObject proxy);
     static JSString *fun_toString(JSContext *cx, HandleObject proxy, unsigned indent);
     static bool regexp_toShared(JSContext *cx, HandleObject proxy, RegExpGuard *g);
     static bool defaultValue(JSContext *cx, HandleObject obj, JSType hint, MutableHandleValue vp);
     static bool getPrototypeOf(JSContext *cx, HandleObject proxy, MutableHandleObject protop);
 
+    static bool watch(JSContext *cx, JS::HandleObject proxy, JS::HandleId id,
+                      JS::HandleObject callable);
+    static bool unwatch(JSContext *cx, JS::HandleObject proxy, JS::HandleId id);
+
     /* IC entry path for handling __noSuchMethod__ on access. */
     static bool callProp(JSContext *cx, HandleObject proxy, HandleObject reveiver, HandleId id,
                          MutableHandleValue vp);
 
     static JSObject * const LazyProto;
 };
 
 // Use these in places where you don't want to #include vm/ProxyObject.h.
--- a/js/src/vm/ScopeObject.cpp
+++ b/js/src/vm/ScopeObject.cpp
@@ -600,16 +600,17 @@ const Class WithObject::class_ = {
         with_SetProperty,
         with_SetElement,
         with_SetSpecial,
         with_GetGenericAttributes,
         with_SetGenericAttributes,
         with_DeleteProperty,
         with_DeleteElement,
         with_DeleteSpecial,
+        nullptr, nullptr, /* watch/unwatch */
         with_Enumerate,
         with_ThisObject,
     }
 };
 
 /*****************************************************************************/
 
 ClonedBlockObject *
--- a/js/src/vm/TypedArrayObject.cpp
+++ b/js/src/vm/TypedArrayObject.cpp
@@ -3465,16 +3465,17 @@ const Class ArrayBufferObject::class_ = 
         ArrayBufferObject::obj_setProperty,
         ArrayBufferObject::obj_setElement,
         ArrayBufferObject::obj_setSpecial,
         ArrayBufferObject::obj_getGenericAttributes,
         ArrayBufferObject::obj_setGenericAttributes,
         ArrayBufferObject::obj_deleteProperty,
         ArrayBufferObject::obj_deleteElement,
         ArrayBufferObject::obj_deleteSpecial,
+        nullptr, nullptr, /* watch/unwatch */
         ArrayBufferObject::obj_enumerate,
         nullptr,       /* thisObject      */
     }
 };
 
 const JSFunctionSpec ArrayBufferObject::jsfuncs[] = {
     JS_FN("slice", ArrayBufferObject::fun_slice, 2, JSFUN_GENERIC_NATIVE),
     JS_FS_END
@@ -3627,16 +3628,17 @@ IMPL_TYPED_ARRAY_COMBINED_UNWRAPPERS(Flo
         _typedArray##Object::obj_setProperty,                                  \
         _typedArray##Object::obj_setElement,                                   \
         _typedArray##Object::obj_setSpecial,                                   \
         _typedArray##Object::obj_getGenericAttributes,                         \
         _typedArray##Object::obj_setGenericAttributes,                         \
         _typedArray##Object::obj_deleteProperty,                               \
         _typedArray##Object::obj_deleteElement,                                \
         _typedArray##Object::obj_deleteSpecial,                                \
+        nullptr, nullptr, /* watch/unwatch */                                  \
         _typedArray##Object::obj_enumerate,                                    \
         nullptr,             /* thisObject  */                                 \
     }                                                                          \
 }
 
 template<class ArrayType>
 static inline JSObject *
 InitTypedArrayClass(JSContext *cx)
--- a/js/xpconnect/src/XPCWrappedNativeJSOps.cpp
+++ b/js/xpconnect/src/XPCWrappedNativeJSOps.cpp
@@ -734,16 +734,17 @@ const XPCWrappedNativeJSClass XPC_WN_NoH
         nullptr, // setProperty
         nullptr, // setElement
         nullptr, // setSpecial
         nullptr, // getGenericAttributes
         nullptr, // setGenericAttributes
         nullptr, // deleteProperty
         nullptr, // deleteElement
         nullptr, // deleteSpecial
+        nullptr, nullptr, // watch/unwatch
         XPC_WN_JSOp_Enumerate,
         XPC_WN_JSOp_ThisObject,
     }
   },
   0 // interfacesBitmap
 };
 
 
--- a/js/xpconnect/src/xpcprivate.h
+++ b/js/xpconnect/src/xpcprivate.h
@@ -1167,16 +1167,17 @@ XPC_WN_JSOp_ThisObject(JSContext *cx, JS
         nullptr, /* setProperty    */                                         \
         nullptr, /* setElement    */                                          \
         nullptr, /* setSpecial    */                                          \
         nullptr, /* getGenericAttributes  */                                  \
         nullptr, /* setGenericAttributes  */                                  \
         nullptr, /* deleteProperty */                                         \
         nullptr, /* deleteElement */                                          \
         nullptr, /* deleteSpecial */                                          \
+        nullptr, nullptr, /* watch/unwatch */                                 \
         XPC_WN_JSOp_Enumerate,                                                \
         XPC_WN_JSOp_ThisObject,                                               \
     }
 
 #define XPC_WN_NoCall_ObjectOps                                               \
     {                                                                         \
         nullptr, /* lookupGeneric */                                          \
         nullptr, /* lookupProperty */                                         \
@@ -1195,16 +1196,17 @@ XPC_WN_JSOp_ThisObject(JSContext *cx, JS
         nullptr, /* setProperty    */                                         \
         nullptr, /* setElement    */                                          \
         nullptr, /* setSpecial    */                                          \
         nullptr, /* getGenericAttributes  */                                  \
         nullptr, /* setGenericAttributes  */                                  \
         nullptr, /* deleteProperty */                                         \
         nullptr, /* deleteElement */                                          \
         nullptr, /* deleteSpecial */                                          \
+        nullptr, nullptr, /* watch/unwatch */                                 \
         XPC_WN_JSOp_Enumerate,                                                \
         XPC_WN_JSOp_ThisObject,                                               \
     }
 
 // Maybe this macro should check for class->enumerate ==
 // XPC_WN_Shared_Proto_Enumerate or something rather than checking for
 // 4 classes?
 static inline bool IS_PROTO_CLASS(const js::Class *clazz)
--- a/testing/mochitest/b2g-debug.json
+++ b/testing/mochitest/b2g-debug.json
@@ -249,16 +249,17 @@
     "content/xml/document/test/test_bug392338.html":"",
     "content/base/test/csp/test_bothCSPheaders.html":"",
     "content/base/test/test_bug383430.html":"",
     "content/base/test/test_bug422403-2.xhtml":"",
     "content/base/test/test_bug424359-1.html":"",
     "content/base/test/test_bug424359-2.html":"",
     "content/base/test/test_mixed_content_blocker_bug803225.html":"",
     "content/html/document/test/test_non-ascii-cookie.html":"",
+    "content/html/document/test/test_document.watch.html":"expects document.cookie setting to work",
 
     "docshell/test/navigation/test_bug13871.html":"",
     "docshell/test/navigation/test_bug270414.html":"",
     "docshell/test/navigation/test_bug344861.html":"",
     "docshell/test/navigation/test_bug386782.html":"",
     "docshell/test/navigation/test_not-opener.html":"",
     "docshell/test/navigation/test_reserved.html":"",
     "docshell/test/test_bug413310.html":"",
--- a/testing/mochitest/b2g-desktop.json
+++ b/testing/mochitest/b2g-desktop.json
@@ -248,16 +248,17 @@
     "content/base/test/csp/test_bothCSPheaders.html":"",
     "content/base/test/test_bug383430.html":"",
     "content/base/test/test_bug422403-2.xhtml":"",
     "content/base/test/test_bug424359-1.html":"",
     "content/base/test/test_bug424359-2.html":"",
     "content/base/test/test_bug426308.html":"",
     "content/base/test/test_mixed_content_blocker_bug803225.html":"",
     "content/html/document/test/test_non-ascii-cookie.html":"",
+    "content/html/document/test/test_document.watch.html":"expects document.cookie setting to work",
 
     "docshell/test/navigation/test_bug13871.html":"",
     "docshell/test/navigation/test_bug270414.html":"",
     "docshell/test/navigation/test_bug344861.html":"",
     "docshell/test/navigation/test_bug386782.html":"",
     "docshell/test/navigation/test_not-opener.html":"",
     "docshell/test/navigation/test_reserved.html":"",
     "docshell/test/test_bug413310.html":"",
--- a/testing/mochitest/b2g.json
+++ b/testing/mochitest/b2g.json
@@ -252,16 +252,17 @@
     "content/xml/document/test/test_bug392338.html":"",
     "content/base/test/csp/test_bothCSPheaders.html":"",
     "content/base/test/test_bug383430.html":"",
     "content/base/test/test_bug422403-2.xhtml":"",
     "content/base/test/test_bug424359-1.html":"",
     "content/base/test/test_bug424359-2.html":"",
     "content/base/test/test_mixed_content_blocker_bug803225.html":"",
     "content/html/document/test/test_non-ascii-cookie.html":"",
+    "content/html/document/test/test_document.watch.html":"expects document.cookie setting to work",
 
     "docshell/test/navigation/test_bug13871.html":"",
     "docshell/test/navigation/test_bug270414.html":"",
     "docshell/test/navigation/test_bug344861.html":"",
     "docshell/test/navigation/test_bug386782.html":"",
     "docshell/test/navigation/test_not-opener.html":"",
     "docshell/test/navigation/test_reserved.html":"",
     "docshell/test/test_bug413310.html":"",