docs: function Plugin(_url, _manifest, _internalRegistry) {
          var _this = this;
          _manifest = _manifest || {};
          var _created = _manifest.created || Date.now();
          var _headers = _manifest.headers || {};
          var _services = _manifest.services || [];
          var _autostart = _manifest.autostart;
          var _lastModified = _manifest.lastModified || 0;
          var _metadataVersion = _manifest.metadataVersion;
  
          var _state = "installed";
  
          var _deferredStateChange;
          var _deferredLoad;
  
          var _channel;
          var _parent;
          var _remoteServices = {};
  
          var _currentMessageId = 0;
          var _currentObjectId = 0;
  
          var _requestReferences = {};
          var _responseReferences = {};
          var _objectReferences = {};
          var _serviceReferences;
  
  
          function _notify(message) {
              if (!_channel) {
                  return;
              }
              _internalRegistry.postMessage(message, _channel);
          }
  
          function _request(message) {
              if (!_channel) {
                  return new Deferred().reject(new Error("plugin not connected"));
              }
  
              message.id = String(_currentMessageId++);
              var d = new Deferred();
              _responseReferences[message.id] = d;
              d.then(null, function(error) {
                  if (_state === "active" && error instanceof Error && error.name === "Cancel") {
                      _notify({
                          requestId: message.id,
                          method: "cancel",
                          params: error.message ? [error.message] : []
                      });
                  }
              });
  
              var toStr = Object.prototype.toString;
              message.params.forEach(function(param, i) {
                  if (toStr.call(param) === "[object Object]" && !(param instanceof ObjectReference)) {
                      var candidate, methods;
                      for (candidate in param) {
                          if (toStr.call(param[candidate]) === "[object Function]") {
                              methods = methods || [];
                              methods.push(candidate);
                          }
                      }
                      if (methods) {
                          var objectId = _currentObjectId++;
                          _objectReferences[objectId] = param;
                          var removeReference = function() {
                              delete _objectReferences[objectId];
                          };
                          d.then(removeReference, removeReference);
                          message.params[i] = new ObjectReference(objectId, methods);
                      }
                  }
              });
              _internalRegistry.postMessage(message, _channel);
              return d.promise;
          }
  
          function _throwError(messageId, error) {
              if (messageId || messageId === 0) {
                  _notify({
                      id: messageId,
                      result: null,
                      error: error
                  });
              } else {
                  console.log(error);
              }
          }
  
          function _callMethod(messageId, implementation, method, params) {
              params.forEach(function(param, i) {
                  if (param && typeof param.__objectId !== "undefined") {
                      var obj = {};
                      param.__methods.forEach(function(method) {
                          obj[method] = function() {
                              return _request({
                                  objectId: param.__objectId,
                                  method: method,
                                  params: Array.prototype.slice.call(arguments)
                              });
                          };
                      });
                      params[i] = obj;
                  }
              });
  
              var response = typeof messageId === "undefined" ? null : {
                  id: messageId,
                  result: null,
                  error: null
              };
              try {
                  var promiseOrResult = method.apply(implementation, params);
                  if (!response) {
                      return;
                  }
  
                  if (promiseOrResult && typeof promiseOrResult.then === "function") { //$NON-NLS-0$
                      _requestReferences[messageId] = promiseOrResult;
                      promiseOrResult.then(function(result) {
                          delete _requestReferences[messageId];
                          response.result = result;
                          _notify(response);
                      }, function(error) {
                          if (_requestReferences[messageId]) {
                              delete _requestReferences[messageId];
                              response.error = _serializeError(error);
                              _notify(response);
                          }
                      }, function() {
                          _notify({
                              responseId: messageId,
                              method: "progress",
                              params: Array.prototype.slice.call(arguments)
                          }); //$NON-NLS-0$
                      });
                  } else {
                      response.result = promiseOrResult;
                      _notify(response);
                  }
              } catch (error) {
                  if (response) {
                      response.error = _serializeError(error);
                      _notify(response);
                  }
              }
          }
  
  
          var _update; // this is a forward reference to a function declared above this.update
  
          function _messageHandler(message) {
              try {
                  if (message.method) { // request
                      var method = message.method,
                          params = message.params || [];
                      if ("serviceId" in message) {
                          var service = _serviceReferences[message.serviceId];
                          if (!service) {
                              _throwError(message.id, "service not found");
                          } else if (_isTrusted(service)) {
                          	var serviceRegistry = _internalRegistry.serviceRegistry;
  	                        service = serviceRegistry.getService(service);
  	                        if (method in service) {
  	                            _callMethod(message.id, service, service[method], params);
  	                        } else {
  	                            _throwError(message.id, "method not found");
  	                        }
                      	} else {
                      		_throwError(message.id, "not trusted");
                      	}
                      } else if ("objectId" in message) {
                          var object = _objectReferences[message.objectId];
                          if (!object) {
                              _throwError(message.id, "object not found");
                          } else if (method in object) {
                              _callMethod(message.id, object, object[method], params);
                          } else {
                              _throwError(message.id, "method not found");
                          }
                      } else if ("requestId" in message) {
                          var request = _requestReferences[message.requestId];
                          if (request && method === "cancel" && request.cancel) {
                              request.cancel.apply(request, params);
                          }
                      } else if ("responseId" in message) {
                          var response = _responseReferences[message.responseId];
                          if (response && method === "progress" && response.progress) {
                              response.progress.apply(response, params);
                          }
                      } else if ("loading" === message.method) {
                          _channel.loading();
                      } else {
                          if ("plugin" === message.method) { //$NON-NLS-0$
                              _channel.connected();
                              var manifest = message.params[0];
                              _update({
                                  headers: manifest.headers,
                                  services: manifest.services
                              }).then(function() {
                                  if (_deferredLoad) {
                                      _deferredLoad.resolve(_this);
                                  }
                              });
                              if (manifest.updateRegistry) {
                              	_updateRemoteRegistry();
                          	}
                          } else if ("timeout" === message.method || "error" === message.method) {
                              if (_deferredLoad) {
                                  _deferredLoad.reject(message.error);
                              }
                          } else {
                              throw new Error("Bad method: " + message.method);
                          }
                      }
                  } else if (message.id) {
                      var deferred = _responseReferences[String(message.id)];
                      if (deferred) {
  	                    delete _responseReferences[String(message.id)];
  	                    if (message.error) {
  	                        var error = _internalRegistry.handleServiceError(_this, message.error);
  	                        deferred.reject(error);
  	                    } else {
  	                        deferred.resolve(message.result);
  	                    }
                      }
                  }
              } catch (e) {
                  console.log("Plugin._messageHandler " + e);
              }
          }
  
          function _createServiceProxy(service) {
              var serviceProxy = {};
              if (service.methods) {
                  service.methods.forEach(function(method) {
                      serviceProxy[method] = function() {
                          var message = {
                              serviceId: service.serviceId,
                              method: method,
                              params: Array.prototype.slice.call(arguments)
                          };
                          if (_state === "active") {
                              return _request(message);
                          } else {
                              return _this.start({
                                  "transient": true
                              }).then(function() {
                                  return _request(message);
                              });
                          }
                      };
                  });
  
                  if (serviceProxy.addEventListener && serviceProxy.removeEventListener) {
                      var eventTarget = new EventTarget();
                      var objectId = _currentObjectId++;
                      _objectReferences[objectId] = {
                          handleEvent: eventTarget.dispatchEvent.bind(eventTarget)
                      };
                      var listenerReference = new ObjectReference(objectId, ["handleEvent"]);
  
                      var _addEventListener = serviceProxy.addEventListener;
                      serviceProxy.addEventListener = function(type, listener) {
                          if (!eventTarget._namedListeners[type]) {
                              _addEventListener(type, listenerReference);
                          }
                          eventTarget.addEventListener(type, listener);
                      };
                      var _removeEventListener = serviceProxy.removeEventListener;
                      serviceProxy.removeEventListener = function(type, listener) {
                          eventTarget.removeEventListener(type, listener);
                          if (!eventTarget._namedListeners[type]) {
                              _removeEventListener(type, listenerReference);
                          }
                      };
                  }
              }
              return serviceProxy;
          }
  
          function _createServiceProperties(service) {
              var properties = JSON.parse(JSON.stringify(service.properties));
              properties.__plugin__ = _url; //TODO: eliminate
              var objectClass = service.names || service.type || [];
              if (!Array.isArray(objectClass)) {
                  objectClass = [objectClass];
              }
              properties.objectClass = objectClass;
              return properties;
          }
  
          function _registerService(service) {
              var serviceProxy = _createServiceProxy(service);
              var properties = _createServiceProperties(service);
              var registration = _internalRegistry.registerService(service.names || service.type, serviceProxy, properties);
              _remoteServices[service.serviceId] = {
                  registration: registration,
                  proxy: serviceProxy
              };
          }
          
          function _getPluginData() {
              var services = [];
              Object.keys(_serviceReferences).forEach(function(serviceId) {
                  var serviceReference = _serviceReferences[serviceId];
                  var properties = {};
                  serviceReference.getPropertyKeys().forEach(function(key) {
                  	properties[key] = serviceReference.getProperty(key);
                  });
                  ["service.id", "service.names", "objectClass", "__plugin__"].forEach(function(key) {
  	                delete properties[key];
                  });
                  var methods = [];
                  var serviceRegistry = _internalRegistry.serviceRegistry;
                  var implementation = serviceRegistry.getService(serviceReference);
  	            for (var method in implementation) {
  	                if (typeof implementation[method] === 'function') {
  	                    methods.push(method);
  	                }
  	            }
                  services.push({
                      serviceId: String(serviceReference.getProperty('service.id')),
                      names: serviceReference.getProperty('service.names'),
                      methods: methods,
                      properties: properties
                  });
              });
              return {
                  services: services
              };
          }
          
          function _isTrusted(serviceReference) {
          	var pluginUrl = new URL(_url);
          	// if plugin is from same origin of host page, it has access to all references
          	if (pluginUrl.origin === location.origin) return true;
          	var refUrl = serviceReference.getProperty("__plugin__");
          	// if plugin is from same origin of reference, it has access to this reference
          	return pluginUrl.origin === (refUrl ? new URL(refUrl) : location).origin;
          }
          
          var _registerListener, _unregisterListener, _modifiedListerner;
          function _updateRemoteRegistry() {
          	if (!_serviceReferences) {
          		_serviceReferences = {};
          		var serviceRegistry = _internalRegistry.serviceRegistry;
          		serviceRegistry.getServiceReferences().forEach(function(serviceReference) {
  		        	if (_isTrusted(serviceReference)) {
  		        		_serviceReferences[serviceReference.getProperty('service.id')] = serviceReference;
  	        		}
          		});
          		function updateReference(add, remove, evt) {
          			var serviceReference = evt.serviceReference;
  		        	var id = serviceReference.getProperty('service.id');
  		        	if (!_serviceReferences[id] && _isTrusted(serviceReference)) {
  		        		if (add) _serviceReferences[id] = serviceReference;
  		        		if (remove) delete _serviceReferences[id];
  			        	_updateRemoteRegistry();
  		        	}
  		        }
  		        serviceRegistry.addEventListener("registered", _registerListener = updateReference.bind(null, true, false));
  		        serviceRegistry.addEventListener("unregistered", _unregisterListener = updateReference.bind(null, false, true));
  		        serviceRegistry.addEventListener("modified", _modifiedListerner = updateReference.bind(null, false, false));
          	}
      		_request({
                  method: "plugin",
                  params: [_getPluginData()]
               });
          }
          
          function _removeListeners() {
      		var serviceRegistry = _internalRegistry.serviceRegistry;
      		if (_registerListener) serviceRegistry.removeEventListener("registered", _registerListener);
  	        if (_unregisterListener) serviceRegistry.removeEventListener("unregistered", _unregisterListener);
  	        if (_modifiedListerner) serviceRegistry.removeEventListener("modified", _modifiedListerner);
  	        _registerListener = _unregisterListener = _modifiedListerner = null;
          }
  
          function _persist() {
              _internalRegistry.persist(_url, {
                  created: _created,
                  headers: _headers,
                  services: _services,
                  autostart: _autostart,
                  metadataVersion: VERSION,
                  lastModified: _lastModified
              });
          }
          
          this._default = false; // used to determine if a plugin is part of the configuration
  
          this._persist = _persist;
  
          this._resolve = function() {
              // check manifest dependencies when we support them
              _state = "resolved";
              _internalRegistry.dispatchEvent(new PluginEvent("resolved", _this));
          };
  
          this._getAutostart = function() {
              return _autostart;
          };
  
          this._getCreated = function() {
              return _created;
          };
  
          /**
           * Returns the URL location of this plugin.
           * @name orion.pluginregistry.Plugin#getLocation
           * @return {String} The URL of this plugin.
           * @function
           */
          this.getLocation = function() {
              return _url;
          };
  
          /**
           * Returns the headers of this plugin.
           * @name orion.pluginregistry.Plugin#getHeaders
           * @return {Object} The plugin headers.
           * @function
           */
          this.getHeaders = function() {
              return JSON.parse(JSON.stringify(_headers));
          };
  
          this.getName = function() {
              var headers = this.getHeaders();
              if (headers) {
                  return headers.name || "";
              }
              return null;
          };
  
          this.getVersion = function() {
              var headers = this.getHeaders();
              if (headers) {
                  return headers.version || "0.0.0";
              }
              return null;
          };
  
          this.getLastModified = function() {
              return _lastModified;
          };
  
          /**
           * Returns the service references provided by this plugin.
           * @name orion.pluginregistry.Plugin#getServiceReferences
           * @return {orion.serviceregistry.ServiceReference[]} The service references provided by this plugin.
           * @function 
           */
          this.getServiceReferences = function() {
              var result = [];
              Object.keys(_remoteServices).forEach(function(serviceId) {
                  result.push(_remoteServices[serviceId].registration.getReference());
              });
              return result;
          };
  
          /**
           * Sets the parent of this plugin.
           * @name orion.pluginregistry.Plugin#setParent
           * @param {DOMElement} [parent=null] the plugin parent. <code>null</code> puts the plugin in the default parent of the plugin registry
           * @return {orion.Promise} A promise that will resolve when the plugin parent has been set.
           * @function 
           */
          this.setParent = function(parent) {
              if (_parent !== parent) {
                  _parent = parent;
                  return _this.stop({
                      "transient": true
                  }).then(function() {
                      if ("started" === _autostart) {
                          return _this.start({
                              "transient": true
                          });
                      } else if ("lazy" === _autostart) {
                          return _this.start({    
                              "lazy": true,
                              "transient": true
                          });
                      }
                  });    
              }
              return new Deferred().resolve();
          };
  
          /**
           * Returns this plugin's current state.
           * @name orion.pluginregistry.Plugin#getState
           * @returns {String} This plugin's state.
           * @function
           */
          this.getState = function() {
              return _state;
          };
          
           /**
           * @name orion.pluginregistry.Plugin#getProblemLoading
           * @description Returns true if there was a problem loading this plug-in, false otherwise. This function is not API and may change in future releases.
           * @private
           * @function
           * @returns {String} Return an true if there was a problem loading this plug-in.
           */
          this.getProblemLoading = function() {
              if (_this._problemLoading){
                  return true;
              }
              return false;
          };
  
          this.start = function(optOptions) {
              if (_state === "uninstalled") {
                  return new Deferred().reject(new Error("Plugin is uninstalled"));
              }
  
              if (_deferredStateChange) {
                  return _deferredStateChange.promise.then(this.start.bind(this, optOptions));
              }
  
              if (_state === "active") {
                  return new Deferred().resolve();
              }
  
              if (!optOptions || !optOptions.transient) {
                  var autostart = optOptions && optOptions.lazy ? "lazy" : "started";
                  if (autostart !== _autostart) {
                      _autostart = autostart;
                      _persist();
                  }
              }
  
              var frameworkState = _internalRegistry.getState();
              if (frameworkState !== "starting" && frameworkState !== "active") {
                  if (optOptions.transient) {
                      return new Deferred().reject(new Error("start transient error"));
                  }
                  return new Deferred().resolve();
              }
  
              if (_state === "installed") {
                  try {
                      this._resolve();
                  } catch (e) {
                      return new Deferred().reject(e);
                  }
              }
  
              if (_state === "resolved") {
                  _services.forEach(function(service) {
                      _registerService(service);
                  });
              }
  
              if (optOptions && optOptions.lazy) {
                  if (_state !== "starting") {
                      _state = "starting";
                      _internalRegistry.dispatchEvent(new PluginEvent("lazy activation", _this));
                  }
                  var now = Date.now();
  		        if (!this.getLastModified() || now > this.getLastModified() + 86400000 || _metadataVersion !== VERSION) { // 24 hours
  	                 return this.update();
                  }
                  return new Deferred().resolve();
              }
              
              var deferredStateChange = new Deferred();
              _deferredStateChange = deferredStateChange;
              _state = "starting";
              _this._problemLoading = null;
              _internalRegistry.dispatchEvent(new PluginEvent("starting", _this));
              _deferredLoad = new Deferred();
              _channel = _internalRegistry.connect(_url, _messageHandler, _parent);
              _deferredLoad.then(function() {
                  _deferredLoad = null;
                  _state = "active";
                  _internalRegistry.dispatchEvent(new PluginEvent("started", _this));
                  _deferredStateChange = null;
                  deferredStateChange.resolve();
              }, function(error) {
                  _deferredLoad = null;
                  _state = "stopping";
                  _internalRegistry.dispatchEvent(new PluginEvent("stopping", _this));
                  Object.keys(_remoteServices).forEach(function(serviceId) {
                      _remoteServices[serviceId].registration.unregister();
                      delete _remoteServices[serviceId];
                  });
                  _internalRegistry.disconnect(_channel);
                  _channel = null;
                  _state = "resolved";
                  _deferredStateChange = null;
                  _internalRegistry.dispatchEvent(new PluginEvent("stopped", _this));
                  _this._problemLoading = true;
                  deferredStateChange.reject(new Error("Failed to load plugin: " + _url + (error && error.message ? "\n    Reason: " + error.message : "")));
                  if (_this._default) {
                      _lastModified = 0;
                      _persist();
                  }
              });
              return deferredStateChange.promise;
          };
  
          this.stop = function(optOptions) {
              if (_state === "uninstalled") {
                  return new Deferred().reject(new Error("Plugin is uninstalled"));
              }
  
              if (_deferredStateChange) {
                  return _deferredStateChange.promise.then(this.stop.bind(this, optOptions));
              }
  
              if (!optOptions || !optOptions.transient) {
                  if ("stopped" !== _autostart) {
                      _autostart = "stopped";
                      _persist();
                  }
              }
  
              if (_state !== "active" && _state !== "starting") {
                  return new Deferred().resolve();
              }
  
              var deferredStateChange = new Deferred();
              _deferredStateChange = deferredStateChange;
  
              _state = "stopping";
              _internalRegistry.dispatchEvent(new PluginEvent("stopping", _this));
              Object.keys(_remoteServices).forEach(function(serviceId) {
                  _remoteServices[serviceId].registration.unregister();
                  delete _remoteServices[serviceId];
              });
              if (_channel) {
                  _internalRegistry.disconnect(_channel);
                  _channel = null;
                  _removeListeners();
              }
              _state = "resolved";
              _deferredStateChange = null;
              _internalRegistry.dispatchEvent(new PluginEvent("stopped", _this));
              deferredStateChange.resolve();
  
              return deferredStateChange.promise;
          };
  
          _update = function(input) {
              _this.problemLoading = null;
              
              if (_state === "uninstalled") {
                  return new Deferred().reject(new Error("Plugin is uninstalled"));
              }
  
              if (!input) {
                  if (_lastModified === 0) {
                      _lastModified = Date.now();
                      _persist();
                  }
                  return _internalRegistry.loadManifest(_url).then(_update, function() {
                      _this._problemLoading = true;
                      if (_this._default) {
                          _lastModified = 0;
                          _persist();
                      }
                      console.log("Failed to load plugin: " + _url);
                  });
              }
  
              var oldHeaders = _headers;
              var oldServices = _services;
              var oldAutostart = _autostart;
              _headers = input.headers || {};
              _services = input.services || [];
              _autostart = input.autostart || _autostart;
  
              if (input.lastModified) {
                  _lastModified = input.lastModified;
              } else {
                  _lastModified = Date.now();
                  _persist();
              }
  
              if (_equal(_headers, oldHeaders) && _equal(_services, oldServices) && _autostart === oldAutostart) {
                  return new Deferred().resolve();
              }
  
              if (_state === "active" || _state === "starting") {
                  var serviceIds = [];
                  Object.keys(_services).forEach(function(serviceId) {
                      var service = _services[serviceId];
                      serviceIds.push(serviceId);
                      var remoteService = _remoteServices[serviceId];
                      if (remoteService) {
                          if (_equal(service.methods, Object.keys(remoteService.proxy))) {
                              var properties = _createServiceProperties(service);
                              var reference = remoteService.registration.getReference();
                              var currentProperties = {};
                              reference.getPropertyKeys().forEach(function(name) {
                                  currentProperties[name] = reference.getProperty(name);
                              });
                              if (!_equal(properties, currentProperties)) {
                                  remoteService.registration.setProperties(properties);
                              }
                              return;
                          }
                          remoteService.registration.unregister();
                          delete _remoteServices[serviceId];
                      }
                      _registerService(service);
                  });
                  Object.keys(_remoteServices).forEach(function(serviceId) {
                      if (serviceIds.indexOf(serviceId) === -1) {
                          _remoteServices[serviceId].registration.unregister();
                          delete _remoteServices[serviceId];
                      }
                  });
              }
  
              if (_state === "active") {
                  _internalRegistry.disconnect(_channel);
                  _deferredLoad = new Deferred();
                  _channel = _internalRegistry.connect(_url, _messageHandler, _parent);
                  _deferredLoad.then(function() {
                      _deferredLoad = null;
                  }, function() {
                      _deferredLoad = null;
                      _state = "stopping";
                      _internalRegistry.dispatchEvent(new PluginEvent("stopping"), _this);
                      Object.keys(_remoteServices).forEach(function(serviceId) {
                          _remoteServices[serviceId].registration.unregister();
                          delete _remoteServices[serviceId];
                      });
                      _internalRegistry.disconnect(_channel);
                      _channel = null;
                      _state = "resolved";
                      _internalRegistry.dispatchEvent(new PluginEvent("stopped", _this));
                  });
              }
              return new Deferred().resolve();
          };
  
          this.update = function(input) {
              return _update(input).then(function() {
                  _internalRegistry.dispatchEvent(new PluginEvent("updated", _this));
              });
          };
          
          /**
           * Uninstalls this plugin.
           * @name orion.pluginregistry.Plugin#uninstall
           * @return {orion.Promise} A promise that will resolve when the plugin has been uninstalled.
           * @function
           */
          this.uninstall = function() {
              if (_state === "uninstalled") {
                  return new Deferred().reject(new Error("Plugin is uninstalled"));
              }
  
              if (_state === "active" || _state === "starting" || _state === "stopping") {
                  return this.stop().then(this.uninstall.bind(this), this.uninstall.bind(this));
              }
  
              _internalRegistry.removePlugin(this);
              _state = "uninstalled";
              _internalRegistry.dispatchEvent(new PluginEvent("uninstalled", _this));
              return new Deferred().resolve();
          };
      }