www

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | Submodules | README | LICENSE

notifier.js (11301B)


      1 /*
      2     ***** BEGIN LICENSE BLOCK *****
      3     
      4     Copyright © 2009 Center for History and New Media
      5                      George Mason University, Fairfax, Virginia, USA
      6                      http://zotero.org
      7     
      8     This file is part of Zotero.
      9     
     10     Zotero is free software: you can redistribute it and/or modify
     11     it under the terms of the GNU Affero General Public License as published by
     12     the Free Software Foundation, either version 3 of the License, or
     13     (at your option) any later version.
     14     
     15     Zotero is distributed in the hope that it will be useful,
     16     but WITHOUT ANY WARRANTY; without even the implied warranty of
     17     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     18     GNU Affero General Public License for more details.
     19     
     20     You should have received a copy of the GNU Affero General Public License
     21     along with Zotero.  If not, see <http://www.gnu.org/licenses/>.
     22     
     23     ***** END LICENSE BLOCK *****
     24 */
     25 
     26 "use strict";
     27 
     28 Zotero.Notifier = new function(){
     29 	var _observers = {};
     30 	var _types = [
     31 		'collection', 'search', 'share', 'share-items', 'item', 'file',
     32 		'collection-item', 'item-tag', 'tag', 'setting', 'group', 'trash',
     33 		'bucket', 'relation', 'feed', 'feedItem', 'sync', 'api-key'
     34 	];
     35 	var _transactionID = false;
     36 	var _queue = {};
     37 
     38 
     39 	/**
     40 	 * @param {Object} [ref] signature {notify: function(event, type, ids, extraData) {}}
     41 	 * @param {Array} [types] a list of types of events observer should be triggered on
     42 	 * @param {String} [id] an id of the observer used in debug output
     43 	 * @param {Integer} [priority] lower numbers correspond to higher priority of observer execution
     44 	 * @returns {string}
     45 	 */
     46 	this.registerObserver = function (ref, types, id, priority) {
     47 		if (types){
     48 			types = Zotero.flattenArguments(types);
     49 			
     50 			for (var i=0; i<types.length; i++){
     51 				if (_types.indexOf(types[i]) == -1){
     52 					throw new Error("Invalid type '" + types[i] + "'");
     53 				}
     54 			}
     55 		}
     56 		
     57 		var len = 2;
     58 		var tries = 10;
     59 		do {
     60 			// Increase the hash length if we can't find a unique key
     61 			if (!tries){
     62 				len++;
     63 				tries = 10;
     64 			}
     65 			
     66 			var hash = (id ? id + '_' : '') + Zotero.randomString(len);
     67 			tries--;
     68 		}
     69 		while (_observers[hash]);
     70 		
     71 		var msg = "Registering notifier observer '" + hash + "' for "
     72 			+ (types ? '[' + types.join() + ']' : 'all types');
     73 		if (priority) {
     74 			msg += " with priority " + priority;
     75 		}
     76 		_observers[hash] = {
     77 			ref: ref,
     78 			types: types,
     79 			priority: priority || false
     80 		};
     81 		return hash;
     82 	}
     83 	
     84 	this.unregisterObserver = function (id) {
     85 		Zotero.debug("Unregistering notifier observer in notifier with id '" + id + "'", 4);
     86 		delete _observers[id];
     87 	}
     88 	
     89 	
     90 	/**
     91 	* Trigger a notification to the appropriate observers
     92 	*
     93 	* Possible values:
     94 	*
     95 	* 	event: 'add', 'modify', 'delete', 'move' ('c', for changing parent),
     96 	*		'remove' (ci, it), 'refresh', 'redraw', 'trash', 'unreadCountUpdated'
     97 	* 	type - 'collection', 'search', 'item', 'collection-item', 'item-tag', 'tag',
     98 	*		'group', 'relation', 'feed', 'feedItem'
     99 	* 	ids - single id or array of ids
    100 	*
    101 	* Notes:
    102 	*
    103 	* - If event queuing is on, events will not fire until commit() is called
    104 	* unless _force_ is true.
    105 	*
    106 	* - New events and types should be added to the order arrays in commit()
    107 	**/
    108 	this.trigger = Zotero.Promise.coroutine(function* (event, type, ids, extraData, force) {
    109 		if (_transactionID && !force) {
    110 			return this.queue(event, type, ids, extraData);
    111 		}
    112 		
    113 		if (_types && _types.indexOf(type) == -1) {
    114 			throw new Error("Invalid type '" + type + "'");
    115 		}
    116 		
    117 		ids = Zotero.flattenArguments(ids);
    118 		
    119 		if (Zotero.Debug.enabled) {
    120 			_logTrigger(event, type, ids, extraData);
    121 		}
    122 		
    123 		var order = _getObserverOrder(type);
    124 		for (let id of order) {
    125 			//Zotero.debug("Calling notify() with " + event + "/" + type
    126 			//	+ " on observer with id '" + id + "'", 5);
    127 			
    128 			if (!_observers[id]) {
    129 				Zotero.debug("Observer no longer exists");
    130 				continue;
    131 			}
    132 			
    133 			// Catch exceptions so all observers get notified even if
    134 			// one throws an error
    135 			try {
    136 				let t = new Date;
    137 				yield Zotero.Promise.resolve(_observers[id].ref.notify(event, type, ids, extraData));
    138 				t = new Date - t;
    139 				if (t > 5) {
    140 					//Zotero.debug(id + " observer finished in " + t + " ms", 5);
    141 				}
    142 			}
    143 			catch (e) {
    144 				Zotero.logError(e);
    145 			}
    146 		}
    147 		
    148 		return true;
    149 	});
    150 	
    151 	
    152 	/**
    153 	 * Queue an event until the end of the current notifier transaction
    154 	 *
    155 	 * Takes the same parameters as trigger()
    156 	 *
    157 	 * @throws If a notifier transaction isn't currently open
    158 	 */
    159 	this.queue = function (event, type, ids, extraData, queue) {
    160 		if (_types && _types.indexOf(type) == -1) {
    161 			throw new Error("Invalid type '" + type + "'");
    162 		}
    163 		
    164 		ids = Zotero.flattenArguments(ids);
    165 		
    166 		if (Zotero.Debug.enabled) {
    167 			_logTrigger(event, type, ids, extraData, true, queue ? queue.id : null);
    168 		}
    169 		
    170 		// Use a queue if one is provided, or else use main queue
    171 		if (queue) {
    172 			queue.size++;
    173 			queue = queue._queue;
    174 		}
    175 		else {
    176 			if (!_transactionID) {
    177 				throw new Error("Can't queue event outside of a transaction");
    178 			}
    179 			queue = _queue;
    180 		}
    181 		
    182 		_mergeEvent(queue, event, type, ids, extraData);
    183 	}
    184 	
    185 	
    186 	function _mergeEvent(queue, event, type, ids, extraData) {
    187 		// Merge with existing queue
    188 		if (!queue[type]) {
    189 			queue[type] = [];
    190 		}
    191 		if (!queue[type][event]) {
    192 			queue[type][event] = {};
    193 		}
    194 		if (!queue[type][event].ids) {
    195 			queue[type][event].ids = [];
    196 			queue[type][event].data = {};
    197 		}
    198 		
    199 		// Merge ids
    200 		queue[type][event].ids = queue[type][event].ids.concat(ids);
    201 		
    202 		// Merge extraData keys
    203 		if (extraData) {
    204 			// If just a single id, extra data can be keyed by id or passed directly
    205 			if (ids.length == 1) {
    206 				let id = ids[0];
    207 				queue[type][event].data[id] = extraData[id] ? extraData[id] : extraData;
    208 			}
    209 			// For multiple ids, check for data keyed by the id
    210 			else {
    211 				for (let i = 0; i < ids.length; i++) {
    212 					let id = ids[i];
    213 					if (extraData[id]) {
    214 						queue[type][event].data[id] = extraData[id];
    215 					}
    216 				}
    217 			}
    218 		}
    219 	}
    220 	
    221 	
    222 	function _logTrigger(event, type, ids, extraData, queueing, queueID) {
    223 		Zotero.debug("Notifier.trigger("
    224 			+ "'" + event + "', "
    225 			+ "'" + type + "', "
    226 			+ "[" + ids.join() + "]"
    227 			+ (extraData ? ", " + JSON.stringify(extraData) : "")
    228 			+ ")"
    229 			+ (queueing
    230 				? " " + (queueID ? "added to queue " + queueID : "queued") + " "
    231 				: " called "
    232 			+ "[observers: " + _countObserversForType(type) + "]")
    233 		);
    234 	}
    235 	
    236 	
    237 	/**
    238 	 * Get order of observer by priority, with lower numbers having higher priority.
    239 	 * If an observer doesn't have a priority, sort it last.
    240 	 */
    241 	function _getObserverOrder(type) {
    242 		var order = [];
    243 		for (let i in _observers) {
    244 			// Skip observers that don't handle notifications for this type (or all types)
    245 			if (_observers[i].types && _observers[i].types.indexOf(type) == -1) {
    246 				continue;
    247 			}
    248 			order.push({
    249 				id: i,
    250 				priority: _observers[i].priority || false
    251 			});
    252 		}
    253 		order.sort((a, b) => {
    254 			if (a.priority === false && b.priority === false) return 0;
    255 			if (a.priority === false) return 1;
    256 			if (b.priority === false) return -1;
    257 			return a.priority - b.priority;
    258 		});
    259 		return order.map(o => o.id);
    260 	}
    261 	
    262 	
    263 	function _countObserversForType(type) {
    264 		var num = 0;
    265 		for (let i in _observers) {
    266 			// Skip observers that don't handle notifications for this type (or all types)
    267 			if (_observers[i].types && _observers[i].types.indexOf(type) == -1) {
    268 				continue;
    269 			}
    270 			num++;
    271 		}
    272 		return num;
    273 	}
    274 	
    275 	
    276 	/**
    277 	 * Begin queueing event notifications (i.e. don't notify the observers)
    278 	 *
    279 	 * Note: Be sure the matching commit() gets called (e.g. in a finally{...} block) or
    280 	 * notifications will break until Firefox is restarted or commit(true)/reset() is called manually
    281 	 *
    282 	 * @param {String} [transactionID]
    283 	 */
    284 	this.begin = function (transactionID = true) {
    285 		_transactionID = transactionID;
    286 	}
    287 	
    288 	
    289 	/**
    290 	 * Send notifications for ids in the event queue
    291 	 *
    292 	 * @param {Zotero.Notifier.Queue|Zotero.Notifier.Queue[]} [queues] - One or more queues to use
    293 	 *     instead of the internal queue
    294 	 * @param {String} [transactionID]
    295 	 */
    296 	this.commit = Zotero.Promise.coroutine(function* (queues, transactionID = true) {
    297 		if (queues) {
    298 			if (!Array.isArray(queues)) {
    299 				queues = [queues];
    300 			}
    301 			
    302 			var queue = {};
    303 			for (let q of queues) {
    304 				q = q._queue;
    305 				for (let type in q) {
    306 					for (let event in q[type]) {
    307 						_mergeEvent(queue, event, type, q[type][event].ids, q[type][event].data);
    308 					}
    309 				}
    310 			}
    311 		}
    312 		else if (!_transactionID) {
    313 			throw new Error("Can't commit outside of transaction");
    314 		}
    315 		else {
    316 			var queue = _queue;
    317 		}
    318 		
    319 		var runQueue = [];
    320 		
    321 		// Sort using order from array, unless missing, in which case sort after
    322 		var getSorter = function (orderArray) {
    323 			return function (a, b) {
    324 				var posA = orderArray.indexOf(a);
    325 				var posB = orderArray.indexOf(b);
    326 				if (posA == -1) posA = 100;
    327 				if (posB == -1) posB = 100;
    328 				return posA - posB;
    329 			}
    330 		};
    331 		
    332 		var typeOrder = ['collection', 'search', 'item', 'collection-item', 'item-tag', 'tag'];
    333 		var eventOrder = ['add', 'modify', 'remove', 'move', 'delete', 'trash'];
    334 		
    335 		var queueTypes = Object.keys(queue);
    336 		queueTypes.sort(getSorter(typeOrder));
    337 		
    338 		var totals = '';
    339 		for (let type of queueTypes) {
    340 			if (!runQueue[type]) {
    341 				runQueue[type] = [];
    342 			}
    343 			
    344 			let typeEvents = Object.keys(queue[type]);
    345 			typeEvents.sort(getSorter(eventOrder));
    346 			
    347 			for (let event of typeEvents) {
    348 				runQueue[type][event] = {
    349 					ids: [],
    350 					data: queue[type][event].data
    351 				};
    352 				
    353 				// Remove redundant ids
    354 				for (let i = 0; i < queue[type][event].ids.length; i++) {
    355 					let id = queue[type][event].ids[i];
    356 					
    357 					// Don't send modify on nonexistent items or tags
    358 					if (event == 'modify') {
    359 						if (type == 'item' && !(yield Zotero.Items.getAsync(id))) {
    360 							continue;
    361 						}
    362 						else if (type == 'tag' && !(yield Zotero.Tags.getAsync(id))) {
    363 							continue;
    364 						}
    365 					}
    366 					
    367 					if (runQueue[type][event].ids.indexOf(id) == -1) {
    368 						runQueue[type][event].ids.push(id);
    369 					}
    370 				}
    371 				
    372 				if (runQueue[type][event].ids.length || event == 'refresh') {
    373 					totals += ' [' + event + '-' + type + ': ' + runQueue[type][event].ids.length + ']';
    374 				}
    375 			}
    376 		}
    377 		
    378 		if (!queues) {
    379 			this.reset(transactionID);
    380 		}
    381 		
    382 		if (totals) {
    383 			if (queues) {
    384 				Zotero.debug("Committing notifier event queues" + totals
    385 					+ " [queues: " + queues.map(q => q.id).join(", ") + "]");
    386 			}
    387 			else {
    388 				Zotero.debug("Committing notifier event queue" + totals);
    389 			}
    390 			
    391 			for (let type in runQueue) {
    392 				for (let event in runQueue[type]) {
    393 					if (runQueue[type][event].ids.length || event == 'refresh') {
    394 						yield this.trigger(
    395 							event,
    396 							type,
    397 							runQueue[type][event].ids,
    398 							runQueue[type][event].data,
    399 							true
    400 						);
    401 					}
    402 				}
    403 			}
    404 		}
    405 	});
    406 	
    407 	
    408 	/*
    409 	 * Reset the event queue
    410 	 */
    411 	this.reset = function (transactionID = true) {
    412 		if (transactionID != _transactionID) {
    413 			return;
    414 		}
    415 		//Zotero.debug("Resetting notifier event queue");
    416 		_queue = {};
    417 		_transactionID = false;
    418 	}
    419 }
    420 
    421 
    422 Zotero.Notifier.Queue = function () {
    423 	this.id = Zotero.Utilities.randomString();
    424 	Zotero.debug("Creating notifier queue " + this.id);
    425 	this._queue = {};
    426 	this.size = 0;
    427 };