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 };