Closes #431, Add Notifier trigger hook to DB.commitTransaction() and queue notifications during transactions

unlock = Zotero.Notifier.begin(lock)
Zotero.Notifier.commit(unlock)
Zotero.Notifier.reset()
Zotero.DB.addCallback(type, callback)

begin(), commit() and reset() are added to beginTransaction(), commitTransaction() and rollbackTransaction(), respectively, on startup, so notifications are now automatically queued during DB transactions -- this has the potential to make complex operations dramatically faster

begin() can also be called manually -- pass true to indicate that intermediate commit()'s (e.g. called from commitTransaction()) shouldn't run the event queue, and pass the value returned to the matching commit() call. (The return value of commit() will be true if it is the first begin() to request a lock and false otherwise -- this allows multiple begin(true) calls to be nested without the nested ones triggering notification.

This does make trigger() order a bit less predictable, but I'm ordering events and types (e.g. calling modify events after add and before delete) in an attempt to avoid problems. We'll see if this works or we need a more sophisticated ordering/grouping scheme.
This commit is contained in:
Dan Stillman 2006-12-08 19:40:54 +00:00
parent 4bf390341a
commit b7b5ff2933
3 changed files with 188 additions and 5 deletions

View File

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

View File

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

View File

@ -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();