zotero/chrome/content/zotero/xpcom/notifier.js
2021-03-02 17:43:10 -05:00

427 lines
11 KiB
JavaScript

/*
***** BEGIN LICENSE BLOCK *****
Copyright © 2009 Center for History and New Media
George Mason University, Fairfax, Virginia, USA
http://zotero.org
This file is part of Zotero.
Zotero is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Zotero is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with Zotero. If not, see <http://www.gnu.org/licenses/>.
***** END LICENSE BLOCK *****
*/
"use strict";
Zotero.Notifier = new function(){
var _observers = {};
var _types = [
'collection', 'search', 'share', 'share-items', 'item', 'file',
'collection-item', 'item-tag', 'tag', 'setting', 'group', 'trash',
'bucket', 'relation', 'feed', 'feedItem', 'sync', 'api-key', 'tab'
];
var _transactionID = false;
var _queue = {};
/**
* @param {Object} [ref] signature {notify: function(event, type, ids, extraData) {}}
* @param {Array} [types] a list of types of events observer should be triggered on
* @param {String} [id] an id of the observer used in debug output
* @param {Integer} [priority] lower numbers correspond to higher priority of observer execution
* @returns {string}
*/
this.registerObserver = function (ref, types, id, priority) {
if (types){
types = Zotero.flattenArguments(types);
for (var i=0; i<types.length; i++){
if (_types.indexOf(types[i]) == -1){
throw new Error("Invalid type '" + types[i] + "'");
}
}
}
var len = 2;
var tries = 10;
do {
// Increase the hash length if we can't find a unique key
if (!tries){
len++;
tries = 10;
}
var hash = (id ? id + '_' : '') + Zotero.randomString(len);
tries--;
}
while (_observers[hash]);
var msg = "Registering notifier observer '" + hash + "' for "
+ (types ? '[' + types.join() + ']' : 'all types');
if (priority) {
msg += " with priority " + priority;
}
_observers[hash] = {
ref: ref,
types: types,
priority: priority || false
};
return hash;
}
this.unregisterObserver = function (id) {
Zotero.debug("Unregistering notifier observer in notifier with id '" + id + "'", 4);
delete _observers[id];
}
/**
* Trigger a notification to the appropriate observers
*
* Possible values:
*
* event: 'add', 'modify', 'delete', 'move' ('c', for changing parent),
* 'remove' (ci, it), 'refresh', 'redraw', 'trash', 'unreadCountUpdated'
* type - 'collection', 'search', 'item', 'collection-item', 'item-tag', 'tag',
* 'group', 'relation', 'feed', 'feedItem'
* ids - single id or array of ids
*
* Notes:
*
* - If event queuing is on, events will not fire until commit() is called
* unless _force_ is true.
*
* - New events and types should be added to the order arrays in commit()
**/
this.trigger = Zotero.Promise.coroutine(function* (event, type, ids, extraData, force) {
if (_transactionID && !force) {
return this.queue(event, type, ids, extraData);
}
if (_types && _types.indexOf(type) == -1) {
throw new Error("Invalid type '" + type + "'");
}
ids = Zotero.flattenArguments(ids);
if (Zotero.Debug.enabled) {
_logTrigger(event, type, ids, extraData);
}
var order = _getObserverOrder(type);
for (let id of order) {
//Zotero.debug("Calling notify() with " + event + "/" + type
// + " on observer with id '" + id + "'", 5);
if (!_observers[id]) {
Zotero.debug("Observer no longer exists");
continue;
}
// Catch exceptions so all observers get notified even if
// one throws an error
try {
let t = new Date;
yield Zotero.Promise.resolve(_observers[id].ref.notify(event, type, ids, extraData));
t = new Date - t;
if (t > 5) {
//Zotero.debug(id + " observer finished in " + t + " ms", 5);
}
}
catch (e) {
Zotero.logError(e);
}
}
return true;
});
/**
* Queue an event until the end of the current notifier transaction
*
* Takes the same parameters as trigger()
*
* @throws If a notifier transaction isn't currently open
*/
this.queue = function (event, type, ids, extraData, queue) {
if (_types && _types.indexOf(type) == -1) {
throw new Error("Invalid type '" + type + "'");
}
ids = Zotero.flattenArguments(ids);
if (Zotero.Debug.enabled) {
_logTrigger(event, type, ids, extraData, true, queue ? queue.id : null);
}
// Use a queue if one is provided, or else use main queue
if (queue) {
queue.size++;
queue = queue._queue;
}
else {
if (!_transactionID) {
throw new Error("Can't queue event outside of a transaction");
}
queue = _queue;
}
_mergeEvent(queue, event, type, ids, extraData);
}
function _mergeEvent(queue, event, type, ids, extraData) {
// Merge with existing queue
if (!queue[type]) {
queue[type] = [];
}
if (!queue[type][event]) {
queue[type][event] = {};
}
if (!queue[type][event].ids) {
queue[type][event].ids = [];
queue[type][event].data = {};
}
// Merge ids
queue[type][event].ids = queue[type][event].ids.concat(ids);
// Merge extraData keys
if (extraData) {
// If just a single id, extra data can be keyed by id or passed directly
if (ids.length == 1) {
let id = ids[0];
queue[type][event].data[id] = extraData[id] ? extraData[id] : extraData;
}
// For multiple ids, check for data keyed by the id
else {
for (let i = 0; i < ids.length; i++) {
let id = ids[i];
if (extraData[id]) {
queue[type][event].data[id] = extraData[id];
}
}
}
}
}
function _logTrigger(event, type, ids, extraData, queueing, queueID) {
Zotero.debug("Notifier.trigger("
+ "'" + event + "', "
+ "'" + type + "', "
+ "[" + ids.join() + "]"
+ (extraData ? ", " + JSON.stringify(extraData) : "")
+ ")"
+ (queueing
? " " + (queueID ? "added to queue " + queueID : "queued") + " "
: " called "
+ "[observers: " + _countObserversForType(type) + "]")
);
}
/**
* Get order of observer by priority, with lower numbers having higher priority.
* If an observer doesn't have a priority, default to 100.
*/
function _getObserverOrder(type) {
var order = [];
for (let i in _observers) {
// Skip observers that don't handle notifications for this type (or all types)
if (_observers[i].types && _observers[i].types.indexOf(type) == -1) {
continue;
}
order.push({
id: i,
priority: _observers[i].priority || 100
});
}
order.sort((a, b) => {
if (a.priority === false && b.priority === false) return 0;
if (a.priority === false) return 1;
if (b.priority === false) return -1;
return a.priority - b.priority;
});
return order.map(o => o.id);
}
function _countObserversForType(type) {
var num = 0;
for (let i in _observers) {
// Skip observers that don't handle notifications for this type (or all types)
if (_observers[i].types && _observers[i].types.indexOf(type) == -1) {
continue;
}
num++;
}
return num;
}
/**
* Begin queueing event notifications (i.e. don't notify the observers)
*
* Note: Be sure the matching commit() gets called (e.g. in a finally{...} block) or
* notifications will break until Firefox is restarted or commit(true)/reset() is called manually
*
* @param {String} [transactionID]
*/
this.begin = function (transactionID = true) {
_transactionID = transactionID;
}
/**
* Send notifications for ids in the event queue
*
* @param {Zotero.Notifier.Queue|Zotero.Notifier.Queue[]} [queues] - One or more queues to use
* instead of the internal queue
* @param {String} [transactionID]
*/
this.commit = Zotero.Promise.coroutine(function* (queues, transactionID = true) {
if (queues) {
if (!Array.isArray(queues)) {
queues = [queues];
}
var queue = {};
for (let q of queues) {
q = q._queue;
for (let type in q) {
for (let event in q[type]) {
_mergeEvent(queue, event, type, q[type][event].ids, q[type][event].data);
}
}
}
}
else if (!_transactionID) {
throw new Error("Can't commit outside of transaction");
}
else {
var queue = _queue;
}
var runQueue = [];
// Sort using order from array, unless missing, in which case sort after
var getSorter = function (orderArray) {
return function (a, b) {
var posA = orderArray.indexOf(a);
var posB = orderArray.indexOf(b);
if (posA == -1) posA = 100;
if (posB == -1) posB = 100;
return posA - posB;
}
};
var typeOrder = ['collection', 'search', 'item', 'collection-item', 'item-tag', 'tag'];
var eventOrder = ['add', 'modify', 'remove', 'move', 'delete', 'trash'];
var queueTypes = Object.keys(queue);
queueTypes.sort(getSorter(typeOrder));
var totals = '';
for (let type of queueTypes) {
if (!runQueue[type]) {
runQueue[type] = [];
}
let typeEvents = Object.keys(queue[type]);
typeEvents.sort(getSorter(eventOrder));
for (let event of typeEvents) {
runQueue[type][event] = {
ids: [],
data: queue[type][event].data
};
// Remove redundant ids
for (let i = 0; i < queue[type][event].ids.length; i++) {
let id = queue[type][event].ids[i];
// Don't send modify on nonexistent items or tags
if (event == 'modify') {
if (type == 'item' && !(yield Zotero.Items.getAsync(id))) {
continue;
}
else if (type == 'tag' && !(yield Zotero.Tags.getAsync(id))) {
continue;
}
}
if (runQueue[type][event].ids.indexOf(id) == -1) {
runQueue[type][event].ids.push(id);
}
}
if (runQueue[type][event].ids.length || event == 'refresh') {
totals += ' [' + event + '-' + type + ': ' + runQueue[type][event].ids.length + ']';
}
}
}
if (!queues) {
this.reset(transactionID);
}
if (totals) {
if (queues) {
Zotero.debug("Committing notifier event queues" + totals
+ " [queues: " + queues.map(q => q.id).join(", ") + "]");
}
else {
Zotero.debug("Committing notifier event queue" + totals);
}
for (let type in runQueue) {
for (let event in runQueue[type]) {
if (runQueue[type][event].ids.length || event == 'refresh') {
yield this.trigger(
event,
type,
runQueue[type][event].ids,
runQueue[type][event].data,
true
);
}
}
}
}
});
/*
* Reset the event queue
*/
this.reset = function (transactionID = true) {
if (transactionID != _transactionID) {
return;
}
//Zotero.debug("Resetting notifier event queue");
_queue = {};
_transactionID = false;
}
}
Zotero.Notifier.Queue = function () {
this.id = Zotero.Utilities.randomString();
Zotero.debug("Creating notifier queue " + this.id);
this._queue = {};
this.size = 0;
};