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
authorJeff Walden <jwalden@mit.edu>
Tue, 29 Oct 2013 16:39:09 -0700
changeset 154269 dad39f51b716
parent 154268 1fa75e9c102a
child 154270 0ae7b31bfa6d
push id25634
push usercbook@mozilla.com
push date2013-11-11 07:54 +0000
treeherdermozilla-central@86afe33114e7 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbhackett, efaust
bugs903332
milestone28.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 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
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.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
@@ -599,16 +599,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,
@@ -948,16 +953,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
@@ -231,16 +231,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;
@@ -574,17 +574,17 @@ obj_watch(JSContext *cx, unsigned argc, 
     if (!ValueToId<CanGC>(cx, args[0], &propid))
         return false;
 
     RootedValue tmp(cx);
     unsigned attrs;
     if (!CheckAccess(cx, obj, propid, JSACC_WATCH, &tmp, &attrs))
         return false;
 
-    if (!JS_SetWatchPoint(cx, obj, propid, obj_watch_handler, callable))
+    if (!JSObject::watch(cx, obj, propid, callable))
         return false;
 
     args.rval().setUndefined();
     return true;
 }
 
 static bool
 obj_unwatch(JSContext *cx, unsigned argc, Value *vp)
@@ -603,17 +603,17 @@ obj_unwatch(JSContext *cx, unsigned argc
     RootedId id(cx);
     if (args.length() != 0) {
         if (!ValueToId<CanGC>(cx, args[0], &id))
             return false;
     } else {
         id = JSID_VOID;
     }
 
-    if (!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 */
 
--- 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
@@ -2164,16 +2164,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,
         nullptr, /* thisObject */
     }
 };
 
 /*static*/ JSObject *
 TypedObject::createZeroed(JSContext *cx, HandleObject type)
 {
@@ -2254,16 +2255,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,
         nullptr, /* thisObject */
     }
 };
 
 const JSFunctionSpec TypedHandle::handleStaticMethods[] = {
     {"move", {nullptr, nullptr}, 3, 0, "HandleMove"},
     {"get", {nullptr, nullptr}, 1, 0, "HandleGet"},
--- a/js/src/jsfriendapi.h
+++ b/js/src/jsfriendapi.h
@@ -1293,16 +1293,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"
@@ -4980,16 +4982,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;
@@ -1085,16 +1091,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
@@ -64,16 +64,31 @@ JSObject::deleteSpecial(JSContext *cx, j
                         bool *succeeded)
 {
     JS::RootedId id(cx, SPECIALID_TO_JSID(sid));
     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
@@ -531,16 +531,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.json
+++ b/testing/mochitest/b2g.json
@@ -253,16 +253,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":"",