fune/browser/components/extensions/ext-bookmarks.js
Andrew Swan f04f785725 Bug 1203330 Part 1 Fix SingletonEventManager r=kmag
This patch adds the ability to run SingletonEventManager handlers in
different modes: sync, async, raw (no exception handling, arg cloning,
or asynchrony), or asyncWithoutClone. When you call the handler,
you're required to specify which variant you want.

Existing uses of SingletonEventManager are all converted to async calls.
Note that some of them were previously synchronous, but it didn't appear
to be necessary.

Also added a callOnClose for SingletonEventManager when the last listener
is removed.

MozReview-Commit-ID: ATHO97dWf3X

--HG--
extra : rebase_source : 43988ee453e965e1c591b1c64603c22f5a103cff
2017-01-26 13:43:12 -08:00

378 lines
10 KiB
JavaScript

/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set sts=2 sw=2 et tw=80: */
"use strict";
const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
Cu.import("resource://gre/modules/ExtensionUtils.jsm");
const {
SingletonEventManager,
} = ExtensionUtils;
XPCOMUtils.defineLazyModuleGetter(this, "EventEmitter",
"resource://devtools/shared/event-emitter.js");
XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
"resource://gre/modules/PlacesUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Task",
"resource://gre/modules/Task.jsm");
let listenerCount = 0;
function getTree(rootGuid, onlyChildren) {
function convert(node, parent) {
let treenode = {
id: node.guid,
title: node.title || "",
index: node.index,
dateAdded: node.dateAdded / 1000,
};
if (parent && node.guid != PlacesUtils.bookmarks.rootGuid) {
treenode.parentId = parent.guid;
}
if (node.type == PlacesUtils.TYPE_X_MOZ_PLACE) {
// This isn't quite correct. Recently Bookmarked ends up here ...
treenode.url = node.uri;
} else {
treenode.dateGroupModified = node.lastModified / 1000;
if (node.children && !onlyChildren) {
treenode.children = node.children.map(child => convert(child, node));
}
}
return treenode;
}
return PlacesUtils.promiseBookmarksTree(rootGuid, {
excludeItemsCallback: item => {
if (item.type == PlacesUtils.TYPE_X_MOZ_PLACE_SEPARATOR) {
return true;
}
return item.annos &&
item.annos.find(a => a.name == PlacesUtils.EXCLUDE_FROM_BACKUP_ANNO);
},
}).then(root => {
if (onlyChildren) {
let children = root.children || [];
return children.map(child => convert(child, root));
}
// It seems like the array always just contains the root node.
return [convert(root, null)];
}).catch(e => Promise.reject({message: e.message}));
}
function convert(result) {
let node = {
id: result.guid,
title: result.title || "",
index: result.index,
dateAdded: result.dateAdded.getTime(),
};
if (result.guid != PlacesUtils.bookmarks.rootGuid) {
node.parentId = result.parentGuid;
}
if (result.type == PlacesUtils.bookmarks.TYPE_BOOKMARK) {
node.url = result.url.href; // Output is always URL object.
} else {
node.dateGroupModified = result.lastModified.getTime();
}
return node;
}
let observer = {
skipTags: true,
skipDescendantsOnItemRemoval: true,
onBeginUpdateBatch() {},
onEndUpdateBatch() {},
onItemAdded(id, parentId, index, itemType, uri, title, dateAdded, guid, parentGuid, source) {
if (itemType == PlacesUtils.bookmarks.TYPE_SEPARATOR) {
return;
}
let bookmark = {
id: guid,
parentId: parentGuid,
index,
title,
dateAdded: dateAdded / 1000,
};
if (itemType == PlacesUtils.bookmarks.TYPE_BOOKMARK) {
bookmark.url = uri.spec;
} else {
bookmark.dateGroupModified = bookmark.dateAdded;
}
this.emit("created", bookmark);
},
onItemVisited() {},
onItemMoved(id, oldParentId, oldIndex, newParentId, newIndex, itemType, guid, oldParentGuid, newParentGuid, source) {
if (itemType == PlacesUtils.bookmarks.TYPE_SEPARATOR) {
return;
}
let info = {
parentId: newParentGuid,
index: newIndex,
oldParentId: oldParentGuid,
oldIndex,
};
this.emit("moved", {guid, info});
},
onItemRemoved(id, parentId, index, itemType, uri, guid, parentGuid, source) {
if (itemType == PlacesUtils.bookmarks.TYPE_SEPARATOR) {
return;
}
let node = {
id: guid,
parentId: parentGuid,
index,
};
if (itemType == PlacesUtils.bookmarks.TYPE_BOOKMARK) {
node.url = uri.spec;
}
this.emit("removed", {guid, info: {parentId: parentGuid, index, node}});
},
onItemChanged(id, prop, isAnno, val, lastMod, itemType, parentId, guid, parentGuid, oldVal, source) {
if (itemType == PlacesUtils.bookmarks.TYPE_SEPARATOR) {
return;
}
let info = {};
if (prop == "title") {
info.title = val;
} else if (prop == "uri") {
info.url = val;
} else {
// Not defined yet.
return;
}
this.emit("changed", {guid, info});
},
};
EventEmitter.decorate(observer);
function decrementListeners() {
listenerCount -= 1;
if (!listenerCount) {
PlacesUtils.bookmarks.removeObserver(observer);
}
}
function incrementListeners() {
listenerCount++;
if (listenerCount == 1) {
PlacesUtils.bookmarks.addObserver(observer, false);
}
}
extensions.registerSchemaAPI("bookmarks", "addon_parent", context => {
return {
bookmarks: {
get: function(idOrIdList) {
let list = Array.isArray(idOrIdList) ? idOrIdList : [idOrIdList];
return Task.spawn(function* () {
let bookmarks = [];
for (let id of list) {
let bookmark = yield PlacesUtils.bookmarks.fetch({guid: id});
if (!bookmark) {
throw new Error("Bookmark not found");
}
bookmarks.push(convert(bookmark));
}
return bookmarks;
}).catch(error => Promise.reject({message: error.message}));
},
getChildren: function(id) {
// TODO: We should optimize this.
return getTree(id, true);
},
getTree: function() {
return getTree(PlacesUtils.bookmarks.rootGuid, false);
},
getSubTree: function(id) {
return getTree(id, false);
},
search: function(query) {
return PlacesUtils.bookmarks.search(query).then(result => result.map(convert));
},
getRecent: function(numberOfItems) {
return PlacesUtils.bookmarks.getRecent(numberOfItems).then(result => result.map(convert));
},
create: function(bookmark) {
let info = {
title: bookmark.title || "",
};
// If url is NULL or missing, it will be a folder.
if (bookmark.url !== null) {
info.type = PlacesUtils.bookmarks.TYPE_BOOKMARK;
info.url = bookmark.url || "";
} else {
info.type = PlacesUtils.bookmarks.TYPE_FOLDER;
}
if (bookmark.index !== null) {
info.index = bookmark.index;
}
if (bookmark.parentId !== null) {
info.parentGuid = bookmark.parentId;
} else {
info.parentGuid = PlacesUtils.bookmarks.unfiledGuid;
}
try {
return PlacesUtils.bookmarks.insert(info).then(convert)
.catch(error => Promise.reject({message: error.message}));
} catch (e) {
return Promise.reject({message: `Invalid bookmark: ${JSON.stringify(info)}`});
}
},
move: function(id, destination) {
let info = {
guid: id,
};
if (destination.parentId !== null) {
info.parentGuid = destination.parentId;
}
info.index = (destination.index === null) ?
PlacesUtils.bookmarks.DEFAULT_INDEX : destination.index;
try {
return PlacesUtils.bookmarks.update(info).then(convert)
.catch(error => Promise.reject({message: error.message}));
} catch (e) {
return Promise.reject({message: `Invalid bookmark: ${JSON.stringify(info)}`});
}
},
update: function(id, changes) {
let info = {
guid: id,
};
if (changes.title !== null) {
info.title = changes.title;
}
if (changes.url !== null) {
info.url = changes.url;
}
try {
return PlacesUtils.bookmarks.update(info).then(convert)
.catch(error => Promise.reject({message: error.message}));
} catch (e) {
return Promise.reject({message: `Invalid bookmark: ${JSON.stringify(info)}`});
}
},
remove: function(id) {
let info = {
guid: id,
};
// The API doesn't give you the old bookmark at the moment
try {
return PlacesUtils.bookmarks.remove(info, {preventRemovalOfNonEmptyFolders: true}).then(result => {})
.catch(error => Promise.reject({message: error.message}));
} catch (e) {
return Promise.reject({message: `Invalid bookmark: ${JSON.stringify(info)}`});
}
},
removeTree: function(id) {
let info = {
guid: id,
};
try {
return PlacesUtils.bookmarks.remove(info).then(result => {})
.catch(error => Promise.reject({message: error.message}));
} catch (e) {
return Promise.reject({message: `Invalid bookmark: ${JSON.stringify(info)}`});
}
},
onCreated: new SingletonEventManager(context, "bookmarks.onCreated", fire => {
let listener = (event, bookmark) => {
// Bug 1333889: make this asynchronous
fire.sync(bookmark.id, bookmark);
};
observer.on("created", listener);
incrementListeners();
return () => {
observer.off("created", listener);
decrementListeners();
};
}).api(),
onRemoved: new SingletonEventManager(context, "bookmarks.onRemoved", fire => {
let listener = (event, data) => {
// Bug 1333889: make this asynchronous
fire.sync(data.guid, data.info);
};
observer.on("removed", listener);
incrementListeners();
return () => {
observer.off("removed", listener);
decrementListeners();
};
}).api(),
onChanged: new SingletonEventManager(context, "bookmarks.onChanged", fire => {
let listener = (event, data) => {
// Bug 1333889: make this asynchronous
fire.sync(data.guid, data.info);
};
observer.on("changed", listener);
incrementListeners();
return () => {
observer.off("changed", listener);
decrementListeners();
};
}).api(),
onMoved: new SingletonEventManager(context, "bookmarks.onMoved", fire => {
let listener = (event, data) => {
// Bug 1333889: make this asynchronous
fire.sync(data.guid, data.info);
};
observer.on("moved", listener);
incrementListeners();
return () => {
observer.off("moved", listener);
decrementListeners();
};
}).api(),
},
};
});