diff --git a/chrome/content/zotero/xpcom/db.js b/chrome/content/zotero/xpcom/db.js index 6936b2c5b6..aabf215361 100644 --- a/chrome/content/zotero/xpcom/db.js +++ b/chrome/content/zotero/xpcom/db.js @@ -25,6 +25,7 @@ Zotero.DB = new function(){ var _connection; var _transactionRollback; var _transactionNestingLevel = 0; + var _callbacks = { begin: [], commit: [], rollback: [] }; this.query = query; this.valueQuery = valueQuery; @@ -36,6 +37,8 @@ Zotero.DB = new function(){ this.beginTransaction = beginTransaction; this.commitTransaction = commitTransaction; this.rollbackTransaction = rollbackTransaction; + this.addCallback = addCallback; + this.removeCallback = removeCallback; this.transactionInProgress = transactionInProgress; this.commitAllTransactions = commitAllTransactions; this.tableExists = tableExists; @@ -288,6 +291,13 @@ Zotero.DB = new function(){ else { Zotero.debug('Beginning DB transaction', 5); db.beginTransaction(); + + // Run callbacks + for (var i=0; i<_callbacks.begin.length; i++) { + if (_callbacks.begin[i]) { + _callbacks.begin[i](); + } + } } } @@ -307,11 +317,18 @@ Zotero.DB = new function(){ Zotero.debug('Committing transaction',5); try { db.commitTransaction(); + + // Run callbacks + for (var i=0; i<_callbacks.commit.length; i++) { + if (_callbacks.commit[i]) { + _callbacks.commit[i](); + } + } } catch(e){ var dberr = (db.lastErrorString!='not an error') ? ' [ERROR: ' + db.lastErrorString + ']' : ''; - throw(e + ' [QUERY: ' + sql + ']' + dberr); + throw(e + dberr); } } } @@ -335,6 +352,13 @@ Zotero.DB = new function(){ _transactionRollback = false; try { db.rollbackTransaction(); + + // Run callbacks + for (var i=0; i<_callbacks.rollback.length; i++) { + if (_callbacks.rollback[i]) { + _callbacks.rollback[i](); + } + } } catch(e){ var dberr = (db.lastErrorString!='not an error') @@ -345,6 +369,38 @@ Zotero.DB = new function(){ } + function addCallback(type, cb) { + switch (type) { + case 'begin': + case 'commit': + case 'rollback': + break; + + default: + throw ("Invalid callback type '" + type + "' in DB.addCallback()"); + } + + var id = _callbacks[type].length; + _callbacks[type][id] = cb; + return id; + } + + + function removeCallback(type, id) { + switch (type) { + case 'begin': + case 'commit': + case 'rollback': + break; + + default: + throw ("Invalid callback type '" + type + "' in DB.removeCallback()"); + } + + delete _callbacks[type][id]; + } + + function transactionInProgress(){ var db = _getDBConnection(); return db.transactionInProgress; diff --git a/chrome/content/zotero/xpcom/notifier.js b/chrome/content/zotero/xpcom/notifier.js index 55b9c3210b..267f8f1f11 100644 --- a/chrome/content/zotero/xpcom/notifier.js +++ b/chrome/content/zotero/xpcom/notifier.js @@ -24,6 +24,9 @@ Zotero.Notifier = new function(){ var _observers = new Zotero.Hash(); var _disabled = false; var _types = ['collection', 'search', 'item']; + var _inTransaction; + var _locked = false; + var _queue = []; this.registerObserver = registerObserver; this.unregisterObserver = unregisterObserver; @@ -32,10 +35,14 @@ Zotero.Notifier = new function(){ this.unregisterCollectionObserver = unregisterCollectionObserver; this.unregisterItemObserver = unregisterItemObserver; this.trigger = trigger; + this.begin = begin; + this.commit = commit; + this.reset = reset; this.disable = disable; this.enable = enable; this.isEnabled = isEnabled; + function registerObserver(ref, types){ if (types){ types = Zotero.flattenArguments(types); @@ -108,8 +115,10 @@ Zotero.Notifier = new function(){ * ids - single id or array of ids * * c = collection, s = search, i = item + * + * New events and types should be added to the order arrays in commit() **/ - function trigger(event, type, ids){ + function trigger(event, type, ids, force){ if (_disabled){ return false; } @@ -120,9 +129,23 @@ Zotero.Notifier = new function(){ ids = Zotero.flattenArguments(ids); - Zotero.debug("Notifier.trigger('" + event + "', '" + type + "', " - + '[' + ids.join() + ']' + ") called " - + "[observers: " + _observers.length + "]"); + var queue = _inTransaction && !force; + + Zotero.debug("Notifier.trigger('" + event + "', '" + type + "', " + '[' + ids.join() + '])' + + (queue ? " queued" : " called " + "[observers: " + _observers.length + "]")); + + if (queue) { + if (!_queue[type]) { + _queue[type] = []; + } + if (!_queue[type][event]) { + _queue[type][event] = []; + } + + _queue[type][event] = _queue[type][event].concat(ids); + + return true; + } for (i in _observers.items){ Zotero.debug("Calling notify() on observer with hash '" + i + "'", 4); @@ -136,6 +159,105 @@ Zotero.Notifier = new function(){ } + /* + * Begin queueing event notifications (i.e. don't notify the observers) + * + * _lock_ will prevent subsequent commits from running the queue until commit() is called + * with the _unlock_ being true + * + * 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 + */ + function begin(lock) { + if (lock && !_locked) { + _locked = true; + var unlock = true; + } + else { + var unlock = false; + } + + if (_inTransaction) { + //Zotero.debug("Notifier queue already open", 4); + } + else { + Zotero.debug("Beginning Notifier event queue"); + _inTransaction = true; + } + + return unlock; + } + + + /* + * Send notifications for ids in the event queue + * + * If the queue is locked, notifications will only run if _unlock_ is true + */ + function commit(unlock) { + // If there's a lock on the event queue and _unlock_ isn't given, don't commit + if ((unlock == undefined && _locked) || (unlock != undefined && !unlock)) { + //Zotero.debug("Keeping Notifier event queue open", 4); + return; + } + + var runQueue = []; + + function sorter(a, b) { + return order.indexOf(a) - order.indexOf(b); + } + var order = ['collection', 'search', 'items']; + _queue.sort(); + + var order = ['add', 'modify', 'remove', 'move', 'delete']; + var totals = ''; + for (var type in _queue) { + if (!runQueue[type]) { + runQueue[type] = []; + } + + _queue[type].sort(); + + for (var event in _queue[type]) { + runQueue[type][event] = []; + + // Remove redundant ids + for each(var id in _queue[type][event]) { + if (runQueue[type][event].indexOf(id) == -1) { + runQueue[type][event].push(id); + } + } + + totals += ' [' + event + '-' + type + ': ' + runQueue[type][event].length + ']'; + } + } + + if (totals) { + Zotero.debug("Committing Notifier event queue" + totals); + + for (var type in runQueue) { + for (var event in runQueue[type]) { + if (runQueue[type][event].length) { + trigger(event, type, runQueue[type][event], true); + } + } + } + } + reset(); + } + + + /* + * Reset the event queue + */ + function reset() { + Zotero.debug("Resetting Notifier event queue"); + _locked = false; + _queue = []; + _inTransaction = false; + } + + function disable(){ Zotero.debug('Disabling Notifier notifications'); _disabled = true; diff --git a/chrome/content/zotero/xpcom/zotero.js b/chrome/content/zotero/xpcom/zotero.js index 5e6bc72b1c..b235f8ccf9 100644 --- a/chrome/content/zotero/xpcom/zotero.js +++ b/chrome/content/zotero/xpcom/zotero.js @@ -122,6 +122,11 @@ var Zotero = new function(){ .getService(Components.interfaces.nsIStringBundleService); _localizedStringBundle = stringBundleService.createBundle(src, appLocale); + // Add notifier queue callbacks to the DB layer + Zotero.DB.addCallback('begin', Zotero.Notifier.begin); + Zotero.DB.addCallback('commit', Zotero.Notifier.commit); + Zotero.DB.addCallback('rollback', Zotero.Notifier.reset); + // Trigger updating of schema and scrapers Zotero.Schema.updateSchema(); Zotero.Schema.updateScrapersRemote();