fune/browser/components/extensions/ext-history.js
Doug Thayer b3eef35a05 Bug 1421703 - replace onVisit with onVisits r=mak
There's a heavy enough overhead to going through XPConnect for
every observer for every visit on the nsINavHistoryObserver
interface, so this patch reduces that by replacing the single-
visit notification with one which accepts an array of visits.

Some notes: To avoid problems with the orderings of the various
ways in which we notify about visits, we have to send our bulk
onVisits notification before doing any of the others. This does
mean it technically behaves slightly different than the prior
approach of interleaving the notifications, but I can't find any
way in which this has any consequences to the end result, and it
doesn't break any tests.

MozReview-Commit-ID: GdeooH8mCkg

--HG--
extra : rebase_source : 48b5f886c4650a756e70f4657cb9d62c8ce40f74
2017-12-20 14:27:24 -08:00

261 lines
8.7 KiB
JavaScript

/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set sts=2 sw=2 et tw=80: */
"use strict";
// The ext-* files are imported into the same scopes.
/* import-globals-from ext-browserAction.js */
XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
"resource://gre/modules/PlacesUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Services",
"resource://gre/modules/Services.jsm");
var {
normalizeTime,
} = ExtensionUtils;
let nsINavHistoryService = Ci.nsINavHistoryService;
const TRANSITION_TO_TRANSITION_TYPES_MAP = new Map([
["link", nsINavHistoryService.TRANSITION_LINK],
["typed", nsINavHistoryService.TRANSITION_TYPED],
["auto_bookmark", nsINavHistoryService.TRANSITION_BOOKMARK],
["auto_subframe", nsINavHistoryService.TRANSITION_EMBED],
["manual_subframe", nsINavHistoryService.TRANSITION_FRAMED_LINK],
["reload", nsINavHistoryService.TRANSITION_RELOAD],
]);
let TRANSITION_TYPE_TO_TRANSITIONS_MAP = new Map();
for (let [transition, transitionType] of TRANSITION_TO_TRANSITION_TYPES_MAP) {
TRANSITION_TYPE_TO_TRANSITIONS_MAP.set(transitionType, transition);
}
const getTransitionType = transition => {
// cannot set a default value for the transition argument as the framework sets it to null
transition = transition || "link";
let transitionType = TRANSITION_TO_TRANSITION_TYPES_MAP.get(transition);
if (!transitionType) {
throw new Error(`|${transition}| is not a supported transition for history`);
}
return transitionType;
};
const getTransition = transitionType => {
return TRANSITION_TYPE_TO_TRANSITIONS_MAP.get(transitionType) || "link";
};
/*
* Converts a nsINavHistoryResultNode into a HistoryItem
*
* https://developer.mozilla.org/en-US/docs/XPCOM_Interface_Reference/nsINavHistoryResultNode
*/
const convertNodeToHistoryItem = node => {
return {
id: node.pageGuid,
url: node.uri,
title: node.title,
lastVisitTime: PlacesUtils.toDate(node.time).getTime(),
visitCount: node.accessCount,
};
};
/*
* Converts a nsINavHistoryResultNode into a VisitItem
*
* https://developer.mozilla.org/en-US/docs/XPCOM_Interface_Reference/nsINavHistoryResultNode
*/
const convertNodeToVisitItem = node => {
return {
id: node.pageGuid,
visitId: String(node.visitId),
visitTime: PlacesUtils.toDate(node.time).getTime(),
referringVisitId: String(node.fromVisitId),
transition: getTransition(node.visitType),
};
};
/*
* Converts a nsINavHistoryContainerResultNode into an array of objects
*
* https://developer.mozilla.org/en-US/docs/XPCOM_Interface_Reference/nsINavHistoryContainerResultNode
*/
const convertNavHistoryContainerResultNode = (container, converter) => {
let results = [];
container.containerOpen = true;
for (let i = 0; i < container.childCount; i++) {
let node = container.getChild(i);
results.push(converter(node));
}
container.containerOpen = false;
return results;
};
var _observer;
const getHistoryObserver = () => {
if (!_observer) {
_observer = new class extends EventEmitter {
onDeleteURI(uri, guid, reason) {
this.emit("visitRemoved", {allHistory: false, urls: [uri.spec]});
}
onVisits(visits) {
for (let visit of visits) {
let data = {
id: visit.guid,
url: visit.uri.spec,
title: visit.lastKnownTitle || "",
lastVisitTime: visit.time / 1000, // time from Places is microseconds,
visitCount: visit.visitCount,
typedCount: visit.typed,
};
this.emit("visited", data);
}
}
onBeginUpdateBatch() {}
onEndUpdateBatch() {}
onTitleChanged(uri, title) {
this.emit("titleChanged", {url: uri.spec, title: title});
}
onClearHistory() {
this.emit("visitRemoved", {allHistory: true, urls: []});
}
onPageChanged() {}
onFrecencyChanged() {}
onManyFrecenciesChanged() {}
onDeleteVisits(uri, time, guid, reason) {
this.emit("visitRemoved", {allHistory: false, urls: [uri.spec]});
}
}();
PlacesUtils.history.addObserver(_observer);
}
return _observer;
};
this.history = class extends ExtensionAPI {
getAPI(context) {
return {
history: {
addUrl: function(details) {
let transition, date;
try {
transition = getTransitionType(details.transition);
} catch (error) {
return Promise.reject({message: error.message});
}
if (details.visitTime) {
date = normalizeTime(details.visitTime);
}
let pageInfo = {
title: details.title,
url: details.url,
visits: [
{
transition,
date,
},
],
};
try {
return PlacesUtils.history.insert(pageInfo).then(() => undefined);
} catch (error) {
return Promise.reject({message: error.message});
}
},
deleteAll: function() {
return PlacesUtils.history.clear();
},
deleteRange: function(filter) {
let newFilter = {
beginDate: normalizeTime(filter.startTime),
endDate: normalizeTime(filter.endTime),
};
// History.removeVisitsByFilter returns a boolean, but our API should return nothing
return PlacesUtils.history.removeVisitsByFilter(newFilter).then(() => undefined);
},
deleteUrl: function(details) {
let url = details.url;
// History.remove returns a boolean, but our API should return nothing
return PlacesUtils.history.remove(url).then(() => undefined);
},
search: function(query) {
let beginTime = (query.startTime == null) ?
PlacesUtils.toPRTime(Date.now() - 24 * 60 * 60 * 1000) :
PlacesUtils.toPRTime(normalizeTime(query.startTime));
let endTime = (query.endTime == null) ?
Number.MAX_VALUE :
PlacesUtils.toPRTime(normalizeTime(query.endTime));
if (beginTime > endTime) {
return Promise.reject({message: "The startTime cannot be after the endTime"});
}
let options = PlacesUtils.history.getNewQueryOptions();
options.includeHidden = true;
options.sortingMode = options.SORT_BY_DATE_DESCENDING;
options.maxResults = query.maxResults || 100;
let historyQuery = PlacesUtils.history.getNewQuery();
historyQuery.searchTerms = query.text;
historyQuery.beginTime = beginTime;
historyQuery.endTime = endTime;
let queryResult = PlacesUtils.history.executeQuery(historyQuery, options).root;
let results = convertNavHistoryContainerResultNode(queryResult, convertNodeToHistoryItem);
return Promise.resolve(results);
},
getVisits: function(details) {
let url = details.url;
if (!url) {
return Promise.reject({message: "A URL must be provided for getVisits"});
}
let options = PlacesUtils.history.getNewQueryOptions();
options.includeHidden = true;
options.sortingMode = options.SORT_BY_DATE_DESCENDING;
options.resultType = options.RESULTS_AS_VISIT;
let historyQuery = PlacesUtils.history.getNewQuery();
historyQuery.uri = Services.io.newURI(url);
let queryResult = PlacesUtils.history.executeQuery(historyQuery, options).root;
let results = convertNavHistoryContainerResultNode(queryResult, convertNodeToVisitItem);
return Promise.resolve(results);
},
onVisited: new EventManager(context, "history.onVisited", fire => {
let listener = (event, data) => {
fire.sync(data);
};
getHistoryObserver().on("visited", listener);
return () => {
getHistoryObserver().off("visited", listener);
};
}).api(),
onVisitRemoved: new EventManager(context, "history.onVisitRemoved", fire => {
let listener = (event, data) => {
fire.sync(data);
};
getHistoryObserver().on("visitRemoved", listener);
return () => {
getHistoryObserver().off("visitRemoved", listener);
};
}).api(),
onTitleChanged: new EventManager(context, "history.onTitleChanged", fire => {
let listener = (event, data) => {
fire.sync(data);
};
getHistoryObserver().on("titleChanged", listener);
return () => {
getHistoryObserver().off("titleChanged", listener);
};
}).api(),
},
};
}
};