gecko-dev/services/common/observers.sys.mjs

149 lines
5.0 KiB
JavaScript

/* 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/. */
/**
* A service for adding, removing and notifying observers of notifications.
* Wraps the nsIObserverService interface.
*
* @version 0.2
*/
export var Observers = {
/**
* Register the given callback as an observer of the given topic.
*
* @param topic {String}
* the topic to observe
*
* @param callback {Object}
* the callback; an Object that implements nsIObserver or a Function
* that gets called when the notification occurs
*
* @param thisObject {Object} [optional]
* the object to use as |this| when calling a Function callback
*
* @returns the observer
*/
add(topic, callback, thisObject) {
let observer = new Observer(topic, callback, thisObject);
this._cache.push(observer);
Services.obs.addObserver(observer, topic, true);
return observer;
},
/**
* Unregister the given callback as an observer of the given topic.
*
* @param topic {String}
* the topic being observed
*
* @param callback {Object}
* the callback doing the observing
*
* @param thisObject {Object} [optional]
* the object being used as |this| when calling a Function callback
*/
remove(topic, callback, thisObject) {
// This seems fairly inefficient, but I'm not sure how much better
// we can make it. We could index by topic, but we can't index by callback
// or thisObject, as far as I know, since the keys to JavaScript hashes
// (a.k.a. objects) can apparently only be primitive values.
let [observer] = this._cache.filter(
v =>
v.topic == topic && v.callback == callback && v.thisObject == thisObject
);
if (observer) {
Services.obs.removeObserver(observer, topic);
this._cache.splice(this._cache.indexOf(observer), 1);
} else {
throw new Error("Attempt to remove non-existing observer");
}
},
/**
* Notify observers about something.
*
* @param topic {String}
* the topic to notify observers about
*
* @param subject {Object} [optional]
* some information about the topic; can be any JS object or primitive
*
* @param data {String} [optional] [deprecated]
* some more information about the topic; deprecated as the subject
* is sufficient to pass all needed information to the JS observers
* that this module targets; if you have multiple values to pass to
* the observer, wrap them in an object and pass them via the subject
* parameter (i.e.: { foo: 1, bar: "some string", baz: myObject })
*/
notify(topic, subject, data) {
subject = typeof subject == "undefined" ? null : new Subject(subject);
data = typeof data == "undefined" ? null : data;
Services.obs.notifyObservers(subject, topic, data);
},
/**
* A cache of observers that have been added.
*
* We use this to remove observers when a caller calls |remove|.
*
* XXX This might result in reference cycles, causing memory leaks,
* if we hold a reference to an observer that holds a reference to us.
* Could we fix that by making this an independent top-level object
* rather than a property of this object?
*/
_cache: [],
};
function Observer(topic, callback, thisObject) {
this.topic = topic;
this.callback = callback;
this.thisObject = thisObject;
}
Observer.prototype = {
QueryInterface: ChromeUtils.generateQI([
"nsIObserver",
"nsISupportsWeakReference",
]),
observe(subject, topic, data) {
// Extract the wrapped object for subjects that are one of our wrappers
// around a JS object. This way we support both wrapped subjects created
// using this module and those that are real XPCOM components.
if (
subject &&
typeof subject == "object" &&
"wrappedJSObject" in subject &&
"observersModuleSubjectWrapper" in subject.wrappedJSObject
) {
subject = subject.wrappedJSObject.object;
}
if (typeof this.callback == "function") {
if (this.thisObject) {
this.callback.call(this.thisObject, subject, data);
} else {
this.callback(subject, data);
}
} else {
// typeof this.callback == "object" (nsIObserver)
this.callback.observe(subject, topic, data);
}
},
};
function Subject(object) {
// Double-wrap the object and set a property identifying the wrappedJSObject
// as one of our wrappers to distinguish between subjects that are one of our
// wrappers (which we should unwrap when notifying our observers) and those
// that are real JS XPCOM components (which we should pass through unaltered).
this.wrappedJSObject = { observersModuleSubjectWrapper: true, object };
}
Subject.prototype = {
QueryInterface: ChromeUtils.generateQI([]),
getScriptableHelper() {},
getInterfaces() {},
};