
If a transaction took over 30 seconds and another transaction timed out waiting for it, the second transaction would reset the notifier queue, but if the first transaction then tried to queue an event, it would fail with this error and roll back. (It would be nice to figure out why transactions are taking over 30 seconds, though.)
428 lines
11 KiB
JavaScript
428 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'
|
|
];
|
|
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, sort it last.
|
|
*/
|
|
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 || false
|
|
});
|
|
}
|
|
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;
|
|
};
|