addon-sdk/source/lib/sdk/places/bookmarks.js

Wed, 31 Dec 2014 06:09:35 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:09:35 +0100
changeset 0
6474c204b198
permissions
-rw-r--r--

Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.

michael@0 1 /* This Source Code Form is subject to the terms of the Mozilla Public
michael@0 2 * License, v. 2.0. If a copy of the MPL was not distributed with this
michael@0 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
michael@0 4
michael@0 5 "use strict";
michael@0 6
michael@0 7 module.metadata = {
michael@0 8 "stability": "unstable",
michael@0 9 "engines": {
michael@0 10 "Firefox": "*"
michael@0 11 }
michael@0 12 };
michael@0 13
michael@0 14 /*
michael@0 15 * Requiring hosts so they can subscribe to client messages
michael@0 16 */
michael@0 17 require('./host/host-bookmarks');
michael@0 18 require('./host/host-tags');
michael@0 19 require('./host/host-query');
michael@0 20
michael@0 21 const { Cc, Ci } = require('chrome');
michael@0 22 const { Class } = require('../core/heritage');
michael@0 23 const { send } = require('../addon/events');
michael@0 24 const { defer, reject, all, resolve, promised } = require('../core/promise');
michael@0 25 const { EventTarget } = require('../event/target');
michael@0 26 const { emit } = require('../event/core');
michael@0 27 const { identity, defer:async } = require('../lang/functional');
michael@0 28 const { extend, merge } = require('../util/object');
michael@0 29 const { fromIterator } = require('../util/array');
michael@0 30 const {
michael@0 31 constructTree, fetchItem, createQuery,
michael@0 32 isRootGroup, createQueryOptions
michael@0 33 } = require('./utils');
michael@0 34 const {
michael@0 35 bookmarkContract, groupContract, separatorContract
michael@0 36 } = require('./contract');
michael@0 37 const bmsrv = Cc["@mozilla.org/browser/nav-bookmarks-service;1"].
michael@0 38 getService(Ci.nsINavBookmarksService);
michael@0 39
michael@0 40 /*
michael@0 41 * Mapping of uncreated bookmarks with their created
michael@0 42 * counterparts
michael@0 43 */
michael@0 44 const itemMap = new WeakMap();
michael@0 45
michael@0 46 /*
michael@0 47 * Constant used by nsIHistoryQuery; 1 is a bookmark query
michael@0 48 * https://developer.mozilla.org/en-US/docs/XPCOM_Interface_Reference/nsINavHistoryQueryOptions
michael@0 49 */
michael@0 50 const BOOKMARK_QUERY = 1;
michael@0 51
michael@0 52 /*
michael@0 53 * Bookmark Item classes
michael@0 54 */
michael@0 55
michael@0 56 const Bookmark = Class({
michael@0 57 extends: [
michael@0 58 bookmarkContract.properties(identity)
michael@0 59 ],
michael@0 60 initialize: function initialize (options) {
michael@0 61 merge(this, bookmarkContract(extend(defaults, options)));
michael@0 62 },
michael@0 63 type: 'bookmark',
michael@0 64 toString: function () '[object Bookmark]'
michael@0 65 });
michael@0 66 exports.Bookmark = Bookmark;
michael@0 67
michael@0 68 const Group = Class({
michael@0 69 extends: [
michael@0 70 groupContract.properties(identity)
michael@0 71 ],
michael@0 72 initialize: function initialize (options) {
michael@0 73 // Don't validate if root group
michael@0 74 if (isRootGroup(options))
michael@0 75 merge(this, options);
michael@0 76 else
michael@0 77 merge(this, groupContract(extend(defaults, options)));
michael@0 78 },
michael@0 79 type: 'group',
michael@0 80 toString: function () '[object Group]'
michael@0 81 });
michael@0 82 exports.Group = Group;
michael@0 83
michael@0 84 const Separator = Class({
michael@0 85 extends: [
michael@0 86 separatorContract.properties(identity)
michael@0 87 ],
michael@0 88 initialize: function initialize (options) {
michael@0 89 merge(this, separatorContract(extend(defaults, options)));
michael@0 90 },
michael@0 91 type: 'separator',
michael@0 92 toString: function () '[object Separator]'
michael@0 93 });
michael@0 94 exports.Separator = Separator;
michael@0 95
michael@0 96 /*
michael@0 97 * Functions
michael@0 98 */
michael@0 99
michael@0 100 function save (items, options) {
michael@0 101 items = [].concat(items);
michael@0 102 options = options || {};
michael@0 103 let emitter = EventTarget();
michael@0 104 let results = [];
michael@0 105 let errors = [];
michael@0 106 let root = constructTree(items);
michael@0 107 let cache = new Map();
michael@0 108
michael@0 109 let isExplicitSave = item => !!~items.indexOf(item);
michael@0 110 // `walk` returns an aggregate promise indicating the completion
michael@0 111 // of the `commitItem` on each node, not whether or not that
michael@0 112 // commit was successful
michael@0 113
michael@0 114 // Force this to be async, as if a ducktype fails validation,
michael@0 115 // the promise implementation will fire an error event, which will
michael@0 116 // not trigger the handler as it's not yet bound
michael@0 117 //
michael@0 118 // Can remove after `Promise.jsm` is implemented in Bug 881047,
michael@0 119 // which will guarantee next tick execution
michael@0 120 async(() => root.walk(preCommitItem).then(commitComplete))();
michael@0 121
michael@0 122 function preCommitItem ({value:item}) {
michael@0 123 // Do nothing if tree root, default group (unsavable),
michael@0 124 // or if it's a dependency and not explicitly saved (in the list
michael@0 125 // of items to be saved), and not needed to be saved
michael@0 126 if (item === null || // node is the tree root
michael@0 127 isRootGroup(item) ||
michael@0 128 (getId(item) && !isExplicitSave(item)))
michael@0 129 return;
michael@0 130
michael@0 131 return promised(validate)(item)
michael@0 132 .then(() => commitItem(item, options))
michael@0 133 .then(data => construct(data, cache))
michael@0 134 .then(savedItem => {
michael@0 135 // If item was just created, make a map between
michael@0 136 // the creation object and created object,
michael@0 137 // so we can reference the item that doesn't have an id
michael@0 138 if (!getId(item))
michael@0 139 saveId(item, savedItem.id);
michael@0 140
michael@0 141 // Emit both the processed item, and original item
michael@0 142 // so a mapping can be understood in handler
michael@0 143 emit(emitter, 'data', savedItem, item);
michael@0 144
michael@0 145 // Push to results iff item was explicitly saved
michael@0 146 if (isExplicitSave(item))
michael@0 147 results[items.indexOf(item)] = savedItem;
michael@0 148 }, reason => {
michael@0 149 // Force reason to be a string for consistency
michael@0 150 reason = reason + '';
michael@0 151 // Emit both the reason, and original item
michael@0 152 // so a mapping can be understood in handler
michael@0 153 emit(emitter, 'error', reason + '', item);
michael@0 154 // Store unsaved item in results list
michael@0 155 results[items.indexOf(item)] = item;
michael@0 156 errors.push(reason);
michael@0 157 });
michael@0 158 }
michael@0 159
michael@0 160 // Called when traversal of the node tree is completed and all
michael@0 161 // items have been committed
michael@0 162 function commitComplete () {
michael@0 163 emit(emitter, 'end', results);
michael@0 164 }
michael@0 165
michael@0 166 return emitter;
michael@0 167 }
michael@0 168 exports.save = save;
michael@0 169
michael@0 170 function search (queries, options) {
michael@0 171 queries = [].concat(queries);
michael@0 172 let emitter = EventTarget();
michael@0 173 let cache = new Map();
michael@0 174 let queryObjs = queries.map(createQuery.bind(null, BOOKMARK_QUERY));
michael@0 175 let optionsObj = createQueryOptions(BOOKMARK_QUERY, options);
michael@0 176
michael@0 177 // Can remove after `Promise.jsm` is implemented in Bug 881047,
michael@0 178 // which will guarantee next tick execution
michael@0 179 async(() => {
michael@0 180 send('sdk-places-query', { queries: queryObjs, options: optionsObj })
michael@0 181 .then(handleQueryResponse);
michael@0 182 })();
michael@0 183
michael@0 184 function handleQueryResponse (data) {
michael@0 185 let deferreds = data.map(item => {
michael@0 186 return construct(item, cache).then(bookmark => {
michael@0 187 emit(emitter, 'data', bookmark);
michael@0 188 return bookmark;
michael@0 189 }, reason => {
michael@0 190 emit(emitter, 'error', reason);
michael@0 191 errors.push(reason);
michael@0 192 });
michael@0 193 });
michael@0 194
michael@0 195 all(deferreds).then(data => {
michael@0 196 emit(emitter, 'end', data);
michael@0 197 }, () => emit(emitter, 'end', []));
michael@0 198 }
michael@0 199
michael@0 200 return emitter;
michael@0 201 }
michael@0 202 exports.search = search;
michael@0 203
michael@0 204 function remove (items) {
michael@0 205 return [].concat(items).map(item => {
michael@0 206 item.remove = true;
michael@0 207 return item;
michael@0 208 });
michael@0 209 }
michael@0 210
michael@0 211 exports.remove = remove;
michael@0 212
michael@0 213 /*
michael@0 214 * Internal Utilities
michael@0 215 */
michael@0 216
michael@0 217 function commitItem (item, options) {
michael@0 218 // Get the item's ID, or getId it's saved version if it exists
michael@0 219 let id = getId(item);
michael@0 220 let data = normalize(item);
michael@0 221 let promise;
michael@0 222
michael@0 223 data.id = id;
michael@0 224
michael@0 225 if (!id) {
michael@0 226 promise = send('sdk-places-bookmarks-create', data);
michael@0 227 } else if (item.remove) {
michael@0 228 promise = send('sdk-places-bookmarks-remove', { id: id });
michael@0 229 } else {
michael@0 230 promise = send('sdk-places-bookmarks-last-updated', {
michael@0 231 id: id
michael@0 232 }).then(function (updated) {
michael@0 233 // If attempting to save an item that is not the
michael@0 234 // latest snapshot of a bookmark item, execute
michael@0 235 // the resolution function
michael@0 236 if (updated !== item.updated && options.resolve)
michael@0 237 return fetchItem(id)
michael@0 238 .then(options.resolve.bind(null, data));
michael@0 239 else
michael@0 240 return data;
michael@0 241 }).then(send.bind(null, 'sdk-places-bookmarks-save'));
michael@0 242 }
michael@0 243
michael@0 244 return promise;
michael@0 245 }
michael@0 246
michael@0 247 /*
michael@0 248 * Turns a bookmark item into a plain object,
michael@0 249 * converts `tags` from Set to Array, group instance to an id
michael@0 250 */
michael@0 251 function normalize (item) {
michael@0 252 let data = merge({}, item);
michael@0 253 // Circumvent prototype property of `type`
michael@0 254 delete data.type;
michael@0 255 data.type = item.type;
michael@0 256 data.tags = [];
michael@0 257 if (item.tags) {
michael@0 258 data.tags = fromIterator(item.tags);
michael@0 259 }
michael@0 260 data.group = getId(data.group) || exports.UNSORTED.id;
michael@0 261
michael@0 262 return data;
michael@0 263 }
michael@0 264
michael@0 265 /*
michael@0 266 * Takes a data object and constructs a BookmarkItem instance
michael@0 267 * of it, recursively generating parent instances as well.
michael@0 268 *
michael@0 269 * Pass in a `cache` Map to reuse instances of
michael@0 270 * bookmark items to reduce overhead;
michael@0 271 * The cache object is a map of id to a deferred with a
michael@0 272 * promise that resolves to the bookmark item.
michael@0 273 */
michael@0 274 function construct (object, cache, forced) {
michael@0 275 let item = instantiate(object);
michael@0 276 let deferred = defer();
michael@0 277
michael@0 278 // Item could not be instantiated
michael@0 279 if (!item)
michael@0 280 return resolve(null);
michael@0 281
michael@0 282 // Return promise for item if found in the cache,
michael@0 283 // and not `forced`. `forced` indicates that this is the construct
michael@0 284 // call that should not read from cache, but should actually perform
michael@0 285 // the construction, as it was set before several async calls
michael@0 286 if (cache.has(item.id) && !forced)
michael@0 287 return cache.get(item.id).promise;
michael@0 288 else if (cache.has(item.id))
michael@0 289 deferred = cache.get(item.id);
michael@0 290 else
michael@0 291 cache.set(item.id, deferred);
michael@0 292
michael@0 293 // When parent group is found in cache, use
michael@0 294 // the same deferred value
michael@0 295 if (item.group && cache.has(item.group)) {
michael@0 296 cache.get(item.group).promise.then(group => {
michael@0 297 item.group = group;
michael@0 298 deferred.resolve(item);
michael@0 299 });
michael@0 300
michael@0 301 // If not in the cache, and a root group, return
michael@0 302 // the premade instance
michael@0 303 } else if (rootGroups.get(item.group)) {
michael@0 304 item.group = rootGroups.get(item.group);
michael@0 305 deferred.resolve(item);
michael@0 306
michael@0 307 // If not in the cache or a root group, fetch the parent
michael@0 308 } else {
michael@0 309 cache.set(item.group, defer());
michael@0 310 fetchItem(item.group).then(group => {
michael@0 311 return construct(group, cache, true);
michael@0 312 }).then(group => {
michael@0 313 item.group = group;
michael@0 314 deferred.resolve(item);
michael@0 315 }, deferred.reject);
michael@0 316 }
michael@0 317
michael@0 318 return deferred.promise;
michael@0 319 }
michael@0 320
michael@0 321 function instantiate (object) {
michael@0 322 if (object.type === 'bookmark')
michael@0 323 return Bookmark(object);
michael@0 324 if (object.type === 'group')
michael@0 325 return Group(object);
michael@0 326 if (object.type === 'separator')
michael@0 327 return Separator(object);
michael@0 328 return null;
michael@0 329 }
michael@0 330
michael@0 331 /**
michael@0 332 * Validates a bookmark item; will throw an error if ininvalid,
michael@0 333 * to be used with `promised`. As bookmark items check on their class,
michael@0 334 * this only checks ducktypes
michael@0 335 */
michael@0 336 function validate (object) {
michael@0 337 if (!isDuckType(object)) return true;
michael@0 338 let contract = object.type === 'bookmark' ? bookmarkContract :
michael@0 339 object.type === 'group' ? groupContract :
michael@0 340 object.type === 'separator' ? separatorContract :
michael@0 341 null;
michael@0 342 if (!contract) {
michael@0 343 throw Error('No type specified');
michael@0 344 }
michael@0 345
michael@0 346 // If object has a property set, and undefined,
michael@0 347 // manually override with default as it'll fail otherwise
michael@0 348 let withDefaults = Object.keys(defaults).reduce((obj, prop) => {
michael@0 349 if (obj[prop] == null) obj[prop] = defaults[prop];
michael@0 350 return obj;
michael@0 351 }, extend(object));
michael@0 352
michael@0 353 contract(withDefaults);
michael@0 354 }
michael@0 355
michael@0 356 function isDuckType (item) {
michael@0 357 return !(item instanceof Bookmark) &&
michael@0 358 !(item instanceof Group) &&
michael@0 359 !(item instanceof Separator);
michael@0 360 }
michael@0 361
michael@0 362 function saveId (unsaved, id) {
michael@0 363 itemMap.set(unsaved, id);
michael@0 364 }
michael@0 365
michael@0 366 // Fetches an item's ID from itself, or from the mapped items
michael@0 367 function getId (item) {
michael@0 368 return typeof item === 'number' ? item :
michael@0 369 item ? item.id || itemMap.get(item) :
michael@0 370 null;
michael@0 371 }
michael@0 372
michael@0 373 /*
michael@0 374 * Set up the default, root groups
michael@0 375 */
michael@0 376
michael@0 377 let defaultGroupMap = {
michael@0 378 MENU: bmsrv.bookmarksMenuFolder,
michael@0 379 TOOLBAR: bmsrv.toolbarFolder,
michael@0 380 UNSORTED: bmsrv.unfiledBookmarksFolder
michael@0 381 };
michael@0 382
michael@0 383 let rootGroups = new Map();
michael@0 384
michael@0 385 for (let i in defaultGroupMap) {
michael@0 386 let group = Object.freeze(Group({ title: i, id: defaultGroupMap[i] }));
michael@0 387 rootGroups.set(defaultGroupMap[i], group);
michael@0 388 exports[i] = group;
michael@0 389 }
michael@0 390
michael@0 391 let defaults = {
michael@0 392 group: exports.UNSORTED,
michael@0 393 index: -1
michael@0 394 };

mercurial