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.

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

mercurial