Source code

Revision control

Other Tools

1
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
2
/* vim: set sts=2 sw=2 et tw=80: */
3
/* This Source Code Form is subject to the terms of the Mozilla Public
4
* License, v. 2.0. If a copy of the MPL was not distributed with this
5
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6
"use strict";
7
8
var EXPORTED_SYMBOLS = ["ExtensionUtils"];
9
10
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
11
const { XPCOMUtils } = ChromeUtils.import(
13
);
14
15
ChromeUtils.defineModuleGetter(
16
this,
17
"setTimeout",
19
);
20
21
// xpcshell doesn't handle idle callbacks well.
22
XPCOMUtils.defineLazyGetter(this, "idleTimeout", () =>
23
Services.appinfo.name === "XPCShell" ? 500 : undefined
24
);
25
26
// It would be nicer to go through `Services.appinfo`, but some tests need to be
27
// able to replace that field with a custom implementation before it is first
28
// called.
29
// eslint-disable-next-line mozilla/use-services
30
const appinfo = Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime);
31
32
let nextId = 0;
33
const uniqueProcessID = appinfo.uniqueProcessID;
34
// Store the process ID in a 16 bit field left shifted to end of a
35
// double's mantissa.
36
// Note: We can't use bitwise ops here, since they truncate to a 32 bit
37
// integer and we need all 53 mantissa bits.
38
const processIDMask = (uniqueProcessID & 0xffff) * 2 ** 37;
39
40
function getUniqueId() {
41
// Note: We can't use bitwise ops here, since they truncate to a 32 bit
42
// integer and we need all 53 mantissa bits.
43
return processIDMask + nextId++;
44
}
45
46
function promiseTimeout(delay) {
47
return new Promise(resolve => setTimeout(resolve, delay));
48
}
49
50
/**
51
* An Error subclass for which complete error messages are always passed
52
* to extensions, rather than being interpreted as an unknown error.
53
*/
54
class ExtensionError extends DOMException {
55
constructor(message) {
56
super(message, "ExtensionError");
57
}
58
// Custom JS classes can't survive IPC, so need to check error name.
59
static [Symbol.hasInstance](e) {
60
return e instanceof DOMException && e.name === "ExtensionError";
61
}
62
}
63
64
function filterStack(error) {
65
return String(error.stack).replace(
66
/(^.*(Task\.jsm|Promise-backend\.js).*\n)+/gm,
67
"<Promise Chain>\n"
68
);
69
}
70
71
/**
72
* Similar to a WeakMap, but creates a new key with the given
73
* constructor if one is not present.
74
*/
75
class DefaultWeakMap extends WeakMap {
76
constructor(defaultConstructor = undefined, init = undefined) {
77
super(init);
78
if (defaultConstructor) {
79
this.defaultConstructor = defaultConstructor;
80
}
81
}
82
83
get(key) {
84
let value = super.get(key);
85
if (value === undefined && !this.has(key)) {
86
value = this.defaultConstructor(key);
87
this.set(key, value);
88
}
89
return value;
90
}
91
}
92
93
class DefaultMap extends Map {
94
constructor(defaultConstructor = undefined, init = undefined) {
95
super(init);
96
if (defaultConstructor) {
97
this.defaultConstructor = defaultConstructor;
98
}
99
}
100
101
get(key) {
102
let value = super.get(key);
103
if (value === undefined && !this.has(key)) {
104
value = this.defaultConstructor(key);
105
this.set(key, value);
106
}
107
return value;
108
}
109
}
110
111
const _winUtils = new DefaultWeakMap(win => {
112
return win.windowUtils;
113
});
114
const getWinUtils = win => _winUtils.get(win);
115
116
function getInnerWindowID(window) {
117
return getWinUtils(window).currentInnerWindowID;
118
}
119
120
/**
121
* A set with a limited number of slots, which flushes older entries as
122
* newer ones are added.
123
*
124
* @param {integer} limit
125
* The maximum size to trim the set to after it grows too large.
126
* @param {integer} [slop = limit * .25]
127
* The number of extra entries to allow in the set after it
128
* reaches the size limit, before it is truncated to the limit.
129
* @param {iterable} [iterable]
130
* An iterable of initial entries to add to the set.
131
*/
132
class LimitedSet extends Set {
133
constructor(limit, slop = Math.round(limit * 0.25), iterable = undefined) {
134
super(iterable);
135
this.limit = limit;
136
this.slop = slop;
137
}
138
139
truncate(limit) {
140
for (let item of this) {
141
// Live set iterators can ge relatively expensive, since they need
142
// to be updated after every modification to the set. Since
143
// breaking out of the loop early will keep the iterator alive
144
// until the next full GC, we're currently better off finishing
145
// the entire loop even after we're done truncating.
146
if (this.size > limit) {
147
this.delete(item);
148
}
149
}
150
}
151
152
add(item) {
153
if (this.size >= this.limit + this.slop && !this.has(item)) {
154
this.truncate(this.limit - 1);
155
}
156
super.add(item);
157
}
158
}
159
160
/**
161
* Returns a Promise which resolves when the given document's DOM has
162
* fully loaded.
163
*
164
* @param {Document} doc The document to await the load of.
165
* @returns {Promise<Document>}
166
*/
167
function promiseDocumentReady(doc) {
168
if (doc.readyState == "interactive" || doc.readyState == "complete") {
169
return Promise.resolve(doc);
170
}
171
172
return new Promise(resolve => {
173
doc.addEventListener(
174
"DOMContentLoaded",
175
function onReady(event) {
176
if (event.target === event.currentTarget) {
177
doc.removeEventListener("DOMContentLoaded", onReady, true);
178
resolve(doc);
179
}
180
},
181
true
182
);
183
});
184
}
185
186
/**
187
* Returns a Promise which resolves when the given window's document's DOM has
188
* fully loaded, the <head> stylesheets have fully loaded, and we have hit an
189
* idle time.
190
*
191
* @param {Window} window The window whose document we will await
192
the readiness of.
193
* @returns {Promise<IdleDeadline>}
194
*/
195
function promiseDocumentIdle(window) {
196
return window.document.documentReadyForIdle.then(() => {
197
return new Promise(resolve =>
198
window.requestIdleCallback(resolve, { timeout: idleTimeout })
199
);
200
});
201
}
202
203
/**
204
* Returns a Promise which resolves when the given document is fully
205
* loaded.
206
*
207
* @param {Document} doc The document to await the load of.
208
* @returns {Promise<Document>}
209
*/
210
function promiseDocumentLoaded(doc) {
211
if (doc.readyState == "complete") {
212
return Promise.resolve(doc);
213
}
214
215
return new Promise(resolve => {
216
doc.defaultView.addEventListener("load", () => resolve(doc), {
217
once: true,
218
});
219
});
220
}
221
222
/**
223
* Returns a Promise which resolves when the given event is dispatched to the
224
* given element.
225
*
226
* @param {Element} element
227
* The element on which to listen.
228
* @param {string} eventName
229
* The event to listen for.
230
* @param {boolean} [useCapture = true]
231
* If true, listen for the even in the capturing rather than
232
* bubbling phase.
233
* @param {Event} [test]
234
* An optional test function which, when called with the
235
* observer's subject and data, should return true if this is the
236
* expected event, false otherwise.
237
* @returns {Promise<Event>}
238
*/
239
function promiseEvent(
240
element,
241
eventName,
242
useCapture = true,
243
test = event => true
244
) {
245
return new Promise(resolve => {
246
function listener(event) {
247
if (test(event)) {
248
element.removeEventListener(eventName, listener, useCapture);
249
resolve(event);
250
}
251
}
252
element.addEventListener(eventName, listener, useCapture);
253
});
254
}
255
256
/**
257
* Returns a Promise which resolves the given observer topic has been
258
* observed.
259
*
260
* @param {string} topic
261
* The topic to observe.
262
* @param {function(nsISupports, string)} [test]
263
* An optional test function which, when called with the
264
* observer's subject and data, should return true if this is the
265
* expected notification, false otherwise.
266
* @returns {Promise<object>}
267
*/
268
function promiseObserved(topic, test = () => true) {
269
return new Promise(resolve => {
270
let observer = (subject, topic, data) => {
271
if (test(subject, data)) {
272
Services.obs.removeObserver(observer, topic);
273
resolve({ subject, data });
274
}
275
};
276
Services.obs.addObserver(observer, topic);
277
});
278
}
279
280
function getMessageManager(target) {
281
if (target.frameLoader) {
282
return target.frameLoader.messageManager;
283
}
284
return target;
285
}
286
287
function flushJarCache(jarPath) {
288
Services.obs.notifyObservers(null, "flush-cache-entry", jarPath);
289
}
290
291
var ExtensionUtils = {
292
flushJarCache,
293
getInnerWindowID,
294
getMessageManager,
295
getUniqueId,
296
filterStack,
297
getWinUtils,
298
promiseDocumentIdle,
299
promiseDocumentLoaded,
300
promiseDocumentReady,
301
promiseEvent,
302
promiseObserved,
303
promiseTimeout,
304
DefaultMap,
305
DefaultWeakMap,
306
ExtensionError,
307
LimitedSet,
308
};