Update translators/styles at startup and on push notifications
Previously, if a translator or style was fixed, people didn't get the fix until their client checked the repository for updates, which could take up to 24 hours. Now, in addition to checking once a day, we check every time Zotero is started and also when we receive a notification from the streaming server, which happens immediately after a translators or style is updated on GitHub. To avoid DDoSing ourselves, the notification includes a random delay (within a given period) before the update is triggered by the client. The streaming server connection is now made when either "Automatically check for updated translators and styles" or "Sync automatically" is enabled. It can be disabled via the extensions.zotero.streaming.enabled pref.
This commit is contained in:
parent
b476c7c7c5
commit
86cf7cbd07
8 changed files with 481 additions and 339 deletions
|
@ -40,7 +40,7 @@ Zotero_Preferences.General = {
|
|||
|
||||
|
||||
updateTranslators: Zotero.Promise.coroutine(function* () {
|
||||
var updated = yield Zotero.Schema.updateFromRepository(true);
|
||||
var updated = yield Zotero.Schema.updateFromRepository(Zotero.Schema.REPO_UPDATE_MANUAL);
|
||||
var button = document.getElementById('updateButton');
|
||||
if (button) {
|
||||
if (updated===-1) {
|
||||
|
|
|
@ -28,6 +28,11 @@ Zotero.Schema = new function(){
|
|||
this.dbInitialized = false;
|
||||
this.goToChangeLog = false;
|
||||
|
||||
this.REPO_UPDATE_MANUAL = 1;
|
||||
this.REPO_UPDATE_UPGRADE = 2;
|
||||
this.REPO_UPDATE_STARTUP = 3;
|
||||
this.REPO_UPDATE_NOTIFICATION = 4;
|
||||
|
||||
var _schemaUpdateDeferred = Zotero.Promise.defer();
|
||||
this.schemaUpdatePromise = _schemaUpdateDeferred.promise;
|
||||
|
||||
|
@ -35,8 +40,12 @@ Zotero.Schema = new function(){
|
|||
var _schemaVersions = [];
|
||||
// Update when adding _updateCompatibility() line to schema update step
|
||||
var _maxCompatibility = 5;
|
||||
var _repositoryTimer;
|
||||
var _remoteUpdateInProgress = false, _localUpdateInProgress = false;
|
||||
|
||||
var _repositoryTimerID;
|
||||
var _repositoryNotificationTimerID;
|
||||
var _nextRepositoryUpdate;
|
||||
var _remoteUpdateInProgress = false;
|
||||
var _localUpdateInProgress = false;
|
||||
|
||||
var self = this;
|
||||
|
||||
|
@ -90,13 +99,14 @@ Zotero.Schema = new function(){
|
|||
.then(function() {
|
||||
(Zotero.isStandalone ? Zotero.uiReadyPromise : Zotero.initializationPromise)
|
||||
.then(1000)
|
||||
.then(function () {
|
||||
return Zotero.Schema.updateBundledFiles();
|
||||
})
|
||||
.then(function () {
|
||||
.then(async function () {
|
||||
await this.updateBundledFiles();
|
||||
if (Zotero.Prefs.get('automaticScraperUpdates')) {
|
||||
await this.updateFromRepository(this.REPO_UPDATE_UPGRADE);
|
||||
}
|
||||
_schemaUpdateDeferred.resolve(true);
|
||||
});
|
||||
});
|
||||
}.bind(this))
|
||||
}.bind(this));
|
||||
}
|
||||
|
||||
// We don't handle upgrades from pre-Zotero 2.1 databases
|
||||
|
@ -203,12 +213,13 @@ Zotero.Schema = new function(){
|
|||
// soon initialization is done so that translation works before the Zotero pane is opened.
|
||||
(Zotero.isStandalone ? Zotero.uiReadyPromise : Zotero.initializationPromise)
|
||||
.then(1000)
|
||||
.then(function () {
|
||||
return Zotero.Schema.updateBundledFiles();
|
||||
})
|
||||
.then(function () {
|
||||
.then(async function () {
|
||||
await this.updateBundledFiles();
|
||||
if (Zotero.Prefs.get('automaticScraperUpdates')) {
|
||||
await this.updateFromRepository(this.REPO_UPDATE_STARTUP);
|
||||
}
|
||||
_schemaUpdateDeferred.resolve(true);
|
||||
});
|
||||
}.bind(this));
|
||||
|
||||
return updated;
|
||||
});
|
||||
|
@ -488,10 +499,12 @@ Zotero.Schema = new function(){
|
|||
case 'styles':
|
||||
yield Zotero.Styles.init(initOpts);
|
||||
var updated = yield _updateBundledFilesAtLocation(installLocation, mode);
|
||||
break;
|
||||
|
||||
case 'translators':
|
||||
yield Zotero.Translators.init(initOpts);
|
||||
var updated = yield _updateBundledFilesAtLocation(installLocation, mode);
|
||||
break;
|
||||
|
||||
default:
|
||||
yield Zotero.Translators.init(initOpts);
|
||||
|
@ -505,14 +518,7 @@ Zotero.Schema = new function(){
|
|||
_localUpdateInProgress = false;
|
||||
}
|
||||
|
||||
if (updated) {
|
||||
if (Zotero.Prefs.get('automaticScraperUpdates')) {
|
||||
yield Zotero.Schema.updateFromRepository(2);
|
||||
}
|
||||
}
|
||||
else {
|
||||
yield Zotero.Schema.updateFromRepository(false);
|
||||
}
|
||||
return updated;
|
||||
});
|
||||
|
||||
/**
|
||||
|
@ -977,19 +983,51 @@ Zotero.Schema = new function(){
|
|||
});
|
||||
|
||||
|
||||
this.onUpdateNotification = async function (delay) {
|
||||
if (!Zotero.Prefs.get('automaticScraperUpdates')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If another repository check -- either from notification or daily check -- is scheduled
|
||||
// before delay, just wait for that one
|
||||
if (_nextRepositoryUpdate) {
|
||||
if (_nextRepositoryUpdate <= (Date.now() + delay)) {
|
||||
Zotero.debug("Next scheduled update from repository is in "
|
||||
+ Math.round((_nextRepositoryUpdate - Date.now()) / 1000) + " seconds "
|
||||
+ "-- ignoring notification");
|
||||
return;
|
||||
}
|
||||
if (_repositoryNotificationTimerID) {
|
||||
clearTimeout(_repositoryNotificationTimerID);
|
||||
}
|
||||
}
|
||||
|
||||
_nextRepositoryUpdate = Date.now() + delay;
|
||||
Zotero.debug(`Updating from repository in ${Math.round(delay / 1000)} seconds`);
|
||||
_repositoryNotificationTimerID = setTimeout(() => {
|
||||
this.updateFromRepository(this.REPO_UPDATE_NOTIFICATION)
|
||||
}, delay);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Send XMLHTTP request for updated translators and styles to the central repository
|
||||
*
|
||||
* @param {Integer} [force=0] - If non-zero, force a repository query regardless of how long it's
|
||||
* been since the last check. 1 means manual update, 2 means forced update after upgrade.
|
||||
* been since the last check. Should be a REPO_UPDATE_* constant.
|
||||
*/
|
||||
this.updateFromRepository = Zotero.Promise.coroutine(function* (force = 0) {
|
||||
if (Zotero.skipBundledFiles) {
|
||||
Zotero.debug("No bundled files -- skipping repository update");
|
||||
return;
|
||||
}
|
||||
|
||||
if (_remoteUpdateInProgress) {
|
||||
Zotero.debug("A remote update is already in progress -- not checking repository");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!force) {
|
||||
if (_remoteUpdateInProgress) {
|
||||
Zotero.debug("A remote update is already in progress -- not checking repository");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check user preference for automatic updates
|
||||
if (!Zotero.Prefs.get('automaticScraperUpdates')) {
|
||||
Zotero.debug('Automatic repository updating disabled -- not checking repository', 4);
|
||||
|
@ -1014,13 +1052,20 @@ Zotero.Schema = new function(){
|
|||
if (_localUpdateInProgress) {
|
||||
Zotero.debug('A local update is already in progress -- delaying repository check', 4);
|
||||
_setRepositoryTimer(600);
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Zotero.locked) {
|
||||
Zotero.debug('Zotero is locked -- delaying repository check', 4);
|
||||
_setRepositoryTimer(600);
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
// If an update from a notification is queued, stop it, since we're updating now
|
||||
if (_repositoryNotificationTimerID) {
|
||||
clearTimeout(_repositoryNotificationTimerID);
|
||||
_repositoryNotificationTimerID = null;
|
||||
_nextRepositoryUpdate = null;
|
||||
}
|
||||
|
||||
if (Zotero.DB.inTransaction()) {
|
||||
|
@ -1029,6 +1074,7 @@ Zotero.Schema = new function(){
|
|||
|
||||
// Get the last timestamp we got from the server
|
||||
var lastUpdated = yield this.getDBVersion('repository');
|
||||
var updated = false;
|
||||
|
||||
try {
|
||||
var url = ZOTERO_CONFIG.REPOSITORY_URL + 'updated?'
|
||||
|
@ -1039,23 +1085,20 @@ Zotero.Schema = new function(){
|
|||
|
||||
_remoteUpdateInProgress = true;
|
||||
|
||||
if (force == 2) {
|
||||
url += '&m=2';
|
||||
}
|
||||
else if (force) {
|
||||
url += '&m=1';
|
||||
if (force) {
|
||||
url += '&m=' + force;
|
||||
}
|
||||
|
||||
// Send list of installed styles
|
||||
var styles = Zotero.Styles.getAll();
|
||||
var styleTimestamps = [];
|
||||
for (var id in styles) {
|
||||
var updated = Zotero.Date.sqlToDate(styles[id].updated);
|
||||
updated = updated ? updated.getTime() / 1000 : 0;
|
||||
for (let id in styles) {
|
||||
let styleUpdated = Zotero.Date.sqlToDate(styles[id].updated);
|
||||
styleUpdated = styleUpdated ? styleUpdated.getTime() / 1000 : 0;
|
||||
var selfLink = styles[id].url;
|
||||
var data = {
|
||||
id: id,
|
||||
updated: updated
|
||||
updated: styleUpdated
|
||||
};
|
||||
if (selfLink) {
|
||||
data.url = selfLink;
|
||||
|
@ -1066,24 +1109,26 @@ Zotero.Schema = new function(){
|
|||
|
||||
try {
|
||||
var xmlhttp = yield Zotero.HTTP.request("POST", url, { body: body });
|
||||
return _updateFromRepositoryCallback(xmlhttp, force);
|
||||
updated = yield _handleRepositoryResponse(xmlhttp, force);
|
||||
}
|
||||
catch (e) {
|
||||
if (e instanceof Zotero.HTTP.UnexpectedStatusException
|
||||
|| e instanceof Zotero.HTTP.BrowserOfflineException) {
|
||||
let msg = " -- retrying in " + ZOTERO_CONFIG.REPOSITORY_RETRY_INTERVAL
|
||||
if (e instanceof Zotero.HTTP.BrowserOfflineException) {
|
||||
Zotero.debug("Browser is offline" + msg, 2);
|
||||
if (!force) {
|
||||
if (e instanceof Zotero.HTTP.UnexpectedStatusException
|
||||
|| e instanceof Zotero.HTTP.BrowserOfflineException) {
|
||||
let msg = " -- retrying in " + ZOTERO_CONFIG.REPOSITORY_RETRY_INTERVAL
|
||||
if (e instanceof Zotero.HTTP.BrowserOfflineException) {
|
||||
Zotero.debug("Browser is offline" + msg, 2);
|
||||
}
|
||||
else {
|
||||
Zotero.logError(e);
|
||||
Zotero.debug(e.status, 1);
|
||||
Zotero.debug(e.xmlhttp.responseText, 1);
|
||||
Zotero.debug("Error updating from repository " + msg, 1);
|
||||
}
|
||||
// TODO: instead, add an observer to start and stop timer on online state change
|
||||
_setRepositoryTimer(ZOTERO_CONFIG.REPOSITORY_RETRY_INTERVAL);
|
||||
return;
|
||||
}
|
||||
else {
|
||||
Zotero.logError(e);
|
||||
Zotero.debug(e.status, 1);
|
||||
Zotero.debug(e.xmlhttp.responseText, 1);
|
||||
Zotero.debug("Error updating from repository " + msg, 1);
|
||||
}
|
||||
// TODO: instead, add an observer to start and stop timer on online state change
|
||||
_setRepositoryTimer(ZOTERO_CONFIG.REPOSITORY_RETRY_INTERVAL);
|
||||
return;
|
||||
}
|
||||
if (xmlhttp) {
|
||||
Zotero.debug(xmlhttp.status, 1);
|
||||
|
@ -1093,16 +1138,28 @@ Zotero.Schema = new function(){
|
|||
};
|
||||
}
|
||||
finally {
|
||||
if (!force) {
|
||||
_setRepositoryTimer(ZOTERO_CONFIG.REPOSITORY_RETRY_INTERVAL);
|
||||
}
|
||||
_remoteUpdateInProgress = false;
|
||||
}
|
||||
|
||||
return updated;
|
||||
});
|
||||
|
||||
|
||||
this.stopRepositoryTimer = function () {
|
||||
if (_repositoryTimer){
|
||||
if (_repositoryTimerID) {
|
||||
Zotero.debug('Stopping repository check timer');
|
||||
_repositoryTimer.cancel();
|
||||
clearTimeout(_repositoryTimerID);
|
||||
_repositoryTimerID = null;
|
||||
}
|
||||
if (_repositoryNotificationTimerID) {
|
||||
Zotero.debug('Stopping repository notification update timer');
|
||||
clearTimeout(_repositoryNotificationTimerID);
|
||||
_repositoryNotificationTimerID = null
|
||||
}
|
||||
_nextRepositoryUpdate = null;
|
||||
}
|
||||
|
||||
|
||||
|
@ -1126,7 +1183,11 @@ Zotero.Schema = new function(){
|
|||
Zotero.getStylesDirectory();
|
||||
|
||||
yield Zotero.Promise.all(Zotero.Translators.reinit(), Zotero.Styles.reinit());
|
||||
yield this.updateBundledFiles();
|
||||
var updated = yield this.updateBundledFiles();
|
||||
if (updated && Zotero.Prefs.get('automaticScraperUpdates')) {
|
||||
yield Zotero.Schema.updateFromRepository(this.REPO_UPDATE_MANUAL);
|
||||
}
|
||||
return updated;
|
||||
});
|
||||
|
||||
|
||||
|
@ -1143,7 +1204,11 @@ Zotero.Schema = new function(){
|
|||
translatorsDir.remove(true);
|
||||
Zotero.getTranslatorsDirectory(); // recreate directory
|
||||
yield Zotero.Translators.reinit();
|
||||
return this.updateBundledFiles('translators');
|
||||
var updated = yield this.updateBundledFiles('translators');
|
||||
if (updated && Zotero.Prefs.get('automaticScraperUpdates')) {
|
||||
yield Zotero.Schema.updateFromRepository(this.REPO_UPDATE_MANUAL);
|
||||
}
|
||||
return updated;
|
||||
});
|
||||
|
||||
|
||||
|
@ -1160,7 +1225,11 @@ Zotero.Schema = new function(){
|
|||
stylesDir.remove(true);
|
||||
Zotero.getStylesDirectory(); // recreate directory
|
||||
yield Zotero.Styles.reinit()
|
||||
return this.updateBundledFiles('styles');
|
||||
var updated = yield this.updateBundledFiles('styles');
|
||||
if (updated && Zotero.Prefs.get('automaticScraperUpdates')) {
|
||||
yield Zotero.Schema.updateFromRepository(this.REPO_UPDATE_MANUAL);
|
||||
}
|
||||
return updated;
|
||||
});
|
||||
|
||||
|
||||
|
@ -1517,7 +1586,7 @@ Zotero.Schema = new function(){
|
|||
*
|
||||
* @return {Promise:Boolean} A promise for whether the update suceeded
|
||||
**/
|
||||
function _updateFromRepositoryCallback(xmlhttp, force) {
|
||||
async function _handleRepositoryResponse(xmlhttp, force) {
|
||||
if (!xmlhttp.responseXML){
|
||||
try {
|
||||
if (xmlhttp.status>1000){
|
||||
|
@ -1532,12 +1601,7 @@ Zotero.Schema = new function(){
|
|||
catch (e){
|
||||
Zotero.debug('Repository cannot be contacted');
|
||||
}
|
||||
|
||||
if (!force) {
|
||||
_setRepositoryTimer(ZOTERO_CONFIG['REPOSITORY_RETRY_INTERVAL']);
|
||||
}
|
||||
|
||||
return Zotero.Promise.resolve(false);
|
||||
return false;
|
||||
}
|
||||
|
||||
var currentTime = xmlhttp.responseXML.
|
||||
|
@ -1657,71 +1721,55 @@ Zotero.Schema = new function(){
|
|||
};
|
||||
|
||||
if (!translatorUpdates.length && !styleUpdates.length){
|
||||
return Zotero.DB.executeTransaction(function* (conn) {
|
||||
await Zotero.DB.executeTransaction(function* (conn) {
|
||||
// Store the timestamp provided by the server
|
||||
yield _updateDBVersion('repository', currentTime);
|
||||
|
||||
// And the local timestamp of the update time
|
||||
yield _updateDBVersion('lastcheck', lastCheckTime);
|
||||
})
|
||||
.then(function () {
|
||||
Zotero.debug('All translators and styles are up-to-date');
|
||||
if (!force) {
|
||||
_setRepositoryTimer(ZOTERO_CONFIG['REPOSITORY_CHECK_INTERVAL']);
|
||||
}
|
||||
});
|
||||
|
||||
Zotero.debug('All translators and styles are up-to-date');
|
||||
if (!force) {
|
||||
_setRepositoryTimer(ZOTERO_CONFIG.REPOSITORY_CHECK_INTERVAL);
|
||||
}
|
||||
updatePDFTools();
|
||||
return true;
|
||||
}
|
||||
|
||||
var updated = false;
|
||||
try {
|
||||
for (var i=0, len=translatorUpdates.length; i<len; i++){
|
||||
await _translatorXMLToFile(translatorUpdates[i]);
|
||||
}
|
||||
|
||||
for (var i=0, len=styleUpdates.length; i<len; i++){
|
||||
await _styleXMLToFile(styleUpdates[i]);
|
||||
}
|
||||
|
||||
// Rebuild caches
|
||||
await Zotero.Translators.reinit({ fromSchemaUpdate: force != 1 });
|
||||
await Zotero.Styles.reinit({ fromSchemaUpdate: force != 1 });
|
||||
|
||||
updated = true;
|
||||
}
|
||||
catch (e) {
|
||||
Zotero.logError(e);
|
||||
}
|
||||
|
||||
if (updated) {
|
||||
await Zotero.DB.executeTransaction(function* (conn) {
|
||||
// Store the timestamp provided by the server
|
||||
yield _updateDBVersion('repository', currentTime);
|
||||
|
||||
return Zotero.Promise.resolve(true);
|
||||
})
|
||||
.tap(function () {
|
||||
updatePDFTools();
|
||||
// And the local timestamp of the update time
|
||||
yield _updateDBVersion('lastcheck', lastCheckTime);
|
||||
});
|
||||
}
|
||||
|
||||
return Zotero.spawn(function* () {
|
||||
try {
|
||||
for (var i=0, len=translatorUpdates.length; i<len; i++){
|
||||
yield _translatorXMLToFile(translatorUpdates[i]);
|
||||
}
|
||||
|
||||
for (var i=0, len=styleUpdates.length; i<len; i++){
|
||||
yield _styleXMLToFile(styleUpdates[i]);
|
||||
}
|
||||
|
||||
// Rebuild caches
|
||||
yield Zotero.Translators.reinit({ fromSchemaUpdate: force != 1 });
|
||||
yield Zotero.Styles.reinit({ fromSchemaUpdate: force != 1 });
|
||||
}
|
||||
catch (e) {
|
||||
Zotero.debug(e, 1);
|
||||
if (!force) {
|
||||
_setRepositoryTimer(ZOTERO_CONFIG['REPOSITORY_RETRY_INTERVAL']);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
})
|
||||
.then(function (update) {
|
||||
if (!update) return false;
|
||||
|
||||
return Zotero.DB.executeTransaction(function* (conn) {
|
||||
// Store the timestamp provided by the server
|
||||
yield _updateDBVersion('repository', currentTime);
|
||||
|
||||
// And the local timestamp of the update time
|
||||
yield _updateDBVersion('lastcheck', lastCheckTime);
|
||||
})
|
||||
.then(function () {
|
||||
if (!force) {
|
||||
_setRepositoryTimer(ZOTERO_CONFIG['REPOSITORY_CHECK_INTERVAL']);
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
})
|
||||
.tap(function () {
|
||||
updatePDFTools();
|
||||
});
|
||||
updatePDFTools();
|
||||
|
||||
return updated;
|
||||
}
|
||||
|
||||
|
||||
|
@ -1730,26 +1778,23 @@ Zotero.Schema = new function(){
|
|||
*
|
||||
* We add an additional two seconds to avoid race conditions
|
||||
**/
|
||||
function _setRepositoryTimer(interval){
|
||||
if (!interval){
|
||||
interval = ZOTERO_CONFIG['REPOSITORY_CHECK_INTERVAL'];
|
||||
}
|
||||
|
||||
function _setRepositoryTimer(delay) {
|
||||
var fudge = 2; // two seconds
|
||||
var displayInterval = interval + fudge;
|
||||
var interval = (interval + fudge) * 1000; // convert to ms
|
||||
var displayInterval = delay + fudge;
|
||||
delay = (delay + fudge) * 1000; // convert to ms
|
||||
|
||||
if (!_repositoryTimer || _repositoryTimer.delay!=interval){
|
||||
Zotero.debug('Setting repository check interval to ' + displayInterval + ' seconds');
|
||||
_repositoryTimer = Components.classes["@mozilla.org/timer;1"].
|
||||
createInstance(Components.interfaces.nsITimer);
|
||||
_repositoryTimer.initWithCallback({
|
||||
// implements nsITimerCallback
|
||||
notify: function(timer){
|
||||
Zotero.Schema.updateFromRepository();
|
||||
}
|
||||
}, interval, Components.interfaces.nsITimer.TYPE_REPEATING_SLACK);
|
||||
if (_repositoryTimerID) {
|
||||
clearTimeout(_repositoryTimerID);
|
||||
_repositoryTimerID = null;
|
||||
}
|
||||
if (_repositoryNotificationTimerID) {
|
||||
clearTimeout(_repositoryNotificationTimerID);
|
||||
_repositoryNotificationTimerID = null;
|
||||
}
|
||||
|
||||
Zotero.debug('Scheduling next repository check in ' + displayInterval + ' seconds');
|
||||
_repositoryTimerID = setTimeout(() => Zotero.Schema.updateFromRepository(), delay);
|
||||
_nextRepositoryUpdate = Date.now() + delay;
|
||||
}
|
||||
|
||||
|
||||
|
|
291
chrome/content/zotero/xpcom/streamer.js
Normal file
291
chrome/content/zotero/xpcom/streamer.js
Normal file
|
@ -0,0 +1,291 @@
|
|||
/*
|
||||
***** BEGIN LICENSE BLOCK *****
|
||||
|
||||
Copyright © 2016 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";
|
||||
|
||||
|
||||
// Initialized as Zotero.Streamer in zotero.js
|
||||
Zotero.Streamer_Module = function (options = {}) {
|
||||
this.url = options.url;
|
||||
this.apiKey = options.apiKey;
|
||||
|
||||
let observer = {
|
||||
notify: function (event, type) {
|
||||
if (event == 'modify') {
|
||||
this.init();
|
||||
}
|
||||
else if (event == 'delete') {
|
||||
this._disconnect();
|
||||
}
|
||||
}.bind(this)
|
||||
};
|
||||
this._observerID = Zotero.Notifier.registerObserver(observer, ['api-key'], 'streamer');
|
||||
};
|
||||
|
||||
Zotero.Streamer_Module.prototype = {
|
||||
_initialized: null,
|
||||
_observerID: null,
|
||||
_socket: null,
|
||||
_ready: false,
|
||||
_reconnect: true,
|
||||
_retry: null,
|
||||
_subscriptions: new Set(),
|
||||
|
||||
|
||||
init: function () {
|
||||
Zotero.Prefs.registerObserver('streaming.enabled', (val) => this._update());
|
||||
Zotero.Prefs.registerObserver('automaticScraperUpdates', (val) => this._update());
|
||||
Zotero.Prefs.registerObserver('sync.autoSync', (val) => this._update());
|
||||
Zotero.uiReadyPromise.then(() => this._update());
|
||||
},
|
||||
|
||||
|
||||
_update: async function () {
|
||||
if (!this._isEnabled()) {
|
||||
this._disconnect();
|
||||
return;
|
||||
}
|
||||
|
||||
// If not connecting or connected, connect now
|
||||
if (!this._socketOpen()) {
|
||||
this._connect();
|
||||
return;
|
||||
}
|
||||
// If not yet ready for messages, wait until we are, at which point this will be called again
|
||||
if (!this._ready) {
|
||||
return;
|
||||
}
|
||||
|
||||
var apiKey = this.apiKey || (await Zotero.Sync.Data.Local.getAPIKey());
|
||||
|
||||
var subscriptionsToAdd = [];
|
||||
var subscriptionsToRemove = [];
|
||||
|
||||
if (Zotero.Prefs.get('sync.autoSync')) {
|
||||
if (!this._subscriptions.has('sync')) {
|
||||
// Subscribe to all topics accessible to the API key
|
||||
subscriptionsToAdd.push({ apiKey });
|
||||
}
|
||||
}
|
||||
else if (this._subscriptions.has('sync')) {
|
||||
subscriptionsToRemove.push({ apiKey });
|
||||
}
|
||||
|
||||
if (Zotero.Prefs.get('automaticScraperUpdates')) {
|
||||
if (!this._subscriptions.has('bundled-files')) {
|
||||
subscriptionsToAdd.push(
|
||||
{
|
||||
topics: ['styles', 'translators']
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
else if (this._subscriptions.has('bundled-files')) {
|
||||
subscriptionsToRemove.push(
|
||||
{
|
||||
topic: 'styles'
|
||||
},
|
||||
{
|
||||
topic: 'translators'
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
if (subscriptionsToAdd.length) {
|
||||
let data = JSON.stringify({
|
||||
action: 'createSubscriptions',
|
||||
subscriptions: subscriptionsToAdd
|
||||
});
|
||||
Zotero.debug("WebSocket message send: " + this._hideAPIKey(data));
|
||||
this._socket.send(data);
|
||||
}
|
||||
if (subscriptionsToRemove.length) {
|
||||
let data = JSON.stringify({
|
||||
action: 'deleteSubscriptions',
|
||||
subscriptions: subscriptionsToRemove
|
||||
});
|
||||
Zotero.debug("WebSocket message send: " + this._hideAPIKey(data));
|
||||
this._socket.send(data);
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
_isEnabled: function () {
|
||||
return Zotero.Prefs.get('streaming.enabled')
|
||||
// Only connect if either auto-sync or automatic style/translator updates are enabled
|
||||
&& (Zotero.Prefs.get('sync.autoSync') || Zotero.Prefs.get('automaticScraperUpdates'));
|
||||
},
|
||||
|
||||
|
||||
_socketOpen: function () {
|
||||
return this._socket && (this._socket.readyState == this._socket.OPEN
|
||||
|| this._socket.readyState == this._socket.CONNECTING);
|
||||
},
|
||||
|
||||
|
||||
_connect: async function () {
|
||||
let url = this.url || Zotero.Prefs.get('streaming.url') || ZOTERO_CONFIG.STREAMING_URL;
|
||||
Zotero.debug(`Connecting to streaming server at ${url}`);
|
||||
|
||||
this._ready = false;
|
||||
this._reconnect = true;
|
||||
|
||||
var window = Cc["@mozilla.org/appshell/appShellService;1"]
|
||||
.getService(Ci.nsIAppShellService).hiddenDOMWindow;
|
||||
this._socket = new window.WebSocket(url, "zotero-streaming-api-v1");
|
||||
var deferred = Zotero.Promise.defer();
|
||||
|
||||
this._socket.onopen = () => {
|
||||
Zotero.debug("WebSocket connection opened");
|
||||
};
|
||||
|
||||
this._socket.onerror = async function (event) {
|
||||
Zotero.debug("WebSocket error");
|
||||
};
|
||||
|
||||
this._socket.onmessage = async function (event) {
|
||||
Zotero.debug("WebSocket message: " + this._hideAPIKey(event.data));
|
||||
|
||||
let data = JSON.parse(event.data);
|
||||
|
||||
if (data.event == "connected") {
|
||||
this._ready = true;
|
||||
this._update();
|
||||
}
|
||||
else {
|
||||
this._reconnectGenerator = null;
|
||||
|
||||
if (data.event == "subscriptionsCreated") {
|
||||
for (let s of data.subscriptions) {
|
||||
if (s.apiKey) {
|
||||
this._subscriptions.add('sync');
|
||||
}
|
||||
else if (s.topics && s.topics.includes('styles')) {
|
||||
this._subscriptions.add('bundled-files');
|
||||
}
|
||||
}
|
||||
|
||||
for (let error of data.errors) {
|
||||
Zotero.logError(this._hideAPIKey(JSON.stringify(error)));
|
||||
}
|
||||
}
|
||||
else if (data.event == "subscriptionsDeleted") {
|
||||
for (let s of data.subscriptions) {
|
||||
if (s.apiKey) {
|
||||
this._subscriptions.delete('sync');
|
||||
}
|
||||
else if (s.topics && s.topics.includes('styles')) {
|
||||
this._subscriptions.delete('bundled-files');
|
||||
}
|
||||
}
|
||||
}
|
||||
// Library added or removed
|
||||
else if (data.event == 'topicAdded' || data.event == 'topicRemoved') {
|
||||
await Zotero.Sync.Runner.sync({
|
||||
background: true
|
||||
});
|
||||
}
|
||||
// Library modified
|
||||
else if (data.event == 'topicUpdated') {
|
||||
// Update translators and styles
|
||||
if (data.topic == 'translators' || data.topic == 'styles') {
|
||||
await Zotero.Schema.onUpdateNotification(data.delay);
|
||||
}
|
||||
// Auto-sync
|
||||
else {
|
||||
let library = Zotero.URI.getPathLibrary(data.topic);
|
||||
if (library) {
|
||||
// Ignore if skipped library
|
||||
let skipped = Zotero.Sync.Data.Local.getSkippedLibraries();
|
||||
if (skipped.includes(library.libraryID)) return;
|
||||
|
||||
await Zotero.Sync.Runner.sync({
|
||||
background: true,
|
||||
libraries: [library.libraryID]
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
// TODO: Handle this in other ways?
|
||||
else if (data.event == 'error') {
|
||||
Zotero.logError(data);
|
||||
}
|
||||
}
|
||||
}.bind(this);
|
||||
|
||||
this._socket.onclose = async function (event) {
|
||||
var msg = `WebSocket connection closed: ${event.code} ${event.reason}`;
|
||||
|
||||
if (event.code != 1000) {
|
||||
Zotero.logError(msg);
|
||||
}
|
||||
else {
|
||||
Zotero.debug(msg);
|
||||
}
|
||||
|
||||
this._subscriptions.clear();
|
||||
|
||||
if (this._reconnect) {
|
||||
if (event.code >= 4400 && event.code < 4500) {
|
||||
Zotero.debug("Not reconnecting to WebSocket due to client error");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this._reconnectGenerator) {
|
||||
let intervals = [
|
||||
2, 5, 10, 15, 30, // first minute
|
||||
60, 60, 60, 60, // every minute for 4 minutes
|
||||
120, 120, 120, 120, // every 2 minutes for 8 minutes
|
||||
300, 300, // every 5 minutes for 10 minutes
|
||||
600, // 10 minutes
|
||||
1200, // 20 minutes
|
||||
1800, 1800, // 30 minutes for 1 hour
|
||||
3600, 3600, 3600, // every hour for 3 hours
|
||||
14400, 14400, 14400, // every 4 hours for 12 hours
|
||||
86400 // 1 day
|
||||
].map(i => i * 1000);
|
||||
this._reconnectGenerator = Zotero.Utilities.Internal.delayGenerator(intervals);
|
||||
}
|
||||
await this._reconnectGenerator.next().value;
|
||||
this._update();
|
||||
}
|
||||
}.bind(this);
|
||||
},
|
||||
|
||||
|
||||
_hideAPIKey: function (str) {
|
||||
return str.replace(/(apiKey":\s*")[^"]+"/, '$1********"');
|
||||
},
|
||||
|
||||
|
||||
_disconnect: function () {
|
||||
this._reconnect = false;
|
||||
this._reconnectGenerator = null;
|
||||
this._subscriptions.clear();
|
||||
if (this._socket) {
|
||||
this._socket.close(1000);
|
||||
}
|
||||
}
|
||||
};
|
|
@ -111,7 +111,6 @@ Zotero.Sync.EventListeners.AutoSyncListener = {
|
|||
|
||||
register: function () {
|
||||
this._observerID = Zotero.Notifier.registerObserver(this, false, 'autosync');
|
||||
Zotero.uiReadyPromise.then(() => Zotero.Sync.Streamer.init());
|
||||
},
|
||||
|
||||
notify: function (event, type, ids, extraData) {
|
||||
|
@ -164,7 +163,6 @@ Zotero.Sync.EventListeners.AutoSyncListener = {
|
|||
if (this._observerID) {
|
||||
Zotero.Notifier.unregisterObserver(this._observerID);
|
||||
}
|
||||
Zotero.Sync.Streamer.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,195 +0,0 @@
|
|||
/*
|
||||
***** BEGIN LICENSE BLOCK *****
|
||||
|
||||
Copyright © 2016 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";
|
||||
|
||||
|
||||
// Initialized as Zotero.Sync.Streamer in zotero.js
|
||||
Zotero.Sync.Streamer_Module = function (options = {}) {
|
||||
this.url = options.url;
|
||||
this.apiKey = options.apiKey;
|
||||
|
||||
let observer = {
|
||||
notify: function (event, type) {
|
||||
if (event == 'modify') {
|
||||
this.init();
|
||||
}
|
||||
else if (event == 'delete') {
|
||||
this.disconnect();
|
||||
}
|
||||
}.bind(this)
|
||||
};
|
||||
this._observerID = Zotero.Notifier.registerObserver(observer, ['api-key'], 'syncStreamer');
|
||||
};
|
||||
|
||||
Zotero.Sync.Streamer_Module.prototype = {
|
||||
_observerID: null,
|
||||
_socket: null,
|
||||
_socketClosedDeferred: null,
|
||||
_reconnect: true,
|
||||
_retry: null,
|
||||
|
||||
init: Zotero.Promise.coroutine(function* () {
|
||||
if (!this._isEnabled()) {
|
||||
return this.disconnect();
|
||||
}
|
||||
|
||||
// If already connected, disconnect first
|
||||
if (this._socket && (this._socket.readyState == this._socket.OPEN
|
||||
|| this._socket.readyState == this._socket.CONNECTING)) {
|
||||
yield this.disconnect();
|
||||
}
|
||||
|
||||
// Connect to the streaming server
|
||||
let apiKey = this.apiKey || (yield Zotero.Sync.Data.Local.getAPIKey());
|
||||
if (apiKey) {
|
||||
let url = this.url || Zotero.Prefs.get('sync.streaming.url') || ZOTERO_CONFIG.STREAMING_URL;
|
||||
this._connect(url, apiKey);
|
||||
}
|
||||
}),
|
||||
|
||||
_isEnabled: function () {
|
||||
return Zotero.Prefs.get('sync.autoSync') && Zotero.Prefs.get('sync.streaming.enabled');
|
||||
},
|
||||
|
||||
_connect: function (url, apiKey) {
|
||||
if (!this._isEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Zotero.debug(`Connecting to streaming server at ${url}`);
|
||||
|
||||
var window = Cc["@mozilla.org/appshell/appShellService;1"]
|
||||
.getService(Ci.nsIAppShellService)
|
||||
.hiddenDOMWindow;
|
||||
this._reconnect = true;
|
||||
|
||||
this._socket = new window.WebSocket(url, "zotero-streaming-api-v1");
|
||||
|
||||
this._socket.onopen = () => {
|
||||
Zotero.debug("WebSocket connection opened");
|
||||
};
|
||||
|
||||
this._socket.onerror = event => {
|
||||
Zotero.debug("WebSocket error");
|
||||
};
|
||||
|
||||
this._socket.onmessage = Zotero.Promise.coroutine(function* (event) {
|
||||
Zotero.debug("WebSocket message: " + this._hideAPIKey(event.data));
|
||||
|
||||
let data = JSON.parse(event.data);
|
||||
|
||||
if (data.event == "connected") {
|
||||
// Subscribe with all topics accessible to the API key
|
||||
let data = JSON.stringify({
|
||||
action: "createSubscriptions",
|
||||
subscriptions: [{ apiKey }]
|
||||
});
|
||||
Zotero.debug("WebSocket message send: " + this._hideAPIKey(data));
|
||||
this._socket.send(data);
|
||||
}
|
||||
else if (data.event == "subscriptionsCreated") {
|
||||
this._reconnectGenerator = null;
|
||||
|
||||
for (let error of data.errors) {
|
||||
Zotero.logError(this._hideAPIKey(JSON.stringify(error)));
|
||||
}
|
||||
}
|
||||
// Library added or removed
|
||||
else if (data.event == 'topicAdded' || data.event == 'topicRemoved') {
|
||||
this._reconnectGenerator = null;
|
||||
|
||||
yield Zotero.Sync.Runner.sync({
|
||||
background: true
|
||||
});
|
||||
}
|
||||
// Library modified
|
||||
else if (data.event == 'topicUpdated') {
|
||||
this._reconnectGenerator = null;
|
||||
|
||||
let library = Zotero.URI.getPathLibrary(data.topic);
|
||||
if (library) {
|
||||
// Ignore if skipped library
|
||||
let skipped = Zotero.Sync.Data.Local.getSkippedLibraries();
|
||||
if (skipped.includes(library.libraryID)) return;
|
||||
|
||||
yield Zotero.Sync.Runner.sync({
|
||||
background: true,
|
||||
libraries: [library.libraryID]
|
||||
});
|
||||
}
|
||||
}
|
||||
}.bind(this));
|
||||
|
||||
this._socket.onclose = Zotero.Promise.coroutine(function* (event) {
|
||||
Zotero.debug(`WebSocket connection closed: ${event.code} ${event.reason}`, 2);
|
||||
|
||||
if (this._socketClosedDeferred) {
|
||||
this._socketClosedDeferred.resolve();
|
||||
}
|
||||
|
||||
if (this._reconnect) {
|
||||
if (event.code >= 4400 && event.code < 4500) {
|
||||
Zotero.debug("Not reconnecting to WebSocket due to client error");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this._reconnectGenerator) {
|
||||
let intervals = [
|
||||
2, 5, 10, 15, 30, // first minute
|
||||
60, 60, 60, 60, // every minute for 4 minutes
|
||||
120, 120, 120, 120, // every 2 minutes for 8 minutes
|
||||
300, 300, // every 5 minutes for 10 minutes
|
||||
600, // 10 minutes
|
||||
1200, // 20 minutes
|
||||
1800, 1800, // 30 minutes for 1 hour
|
||||
3600, 3600, 3600, // every hour for 3 hours
|
||||
14400, 14400, 14400, // every 4 hours for 12 hours
|
||||
86400 // 1 day
|
||||
].map(i => i * 1000);
|
||||
this._reconnectGenerator = Zotero.Utilities.Internal.delayGenerator(intervals);
|
||||
}
|
||||
yield this._reconnectGenerator.next().value;
|
||||
this._connect(url, apiKey);
|
||||
}
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
|
||||
_hideAPIKey: function (str) {
|
||||
return str.replace(/(apiKey":\s*")[^"]+"/, '$1********"');
|
||||
},
|
||||
|
||||
|
||||
disconnect: Zotero.Promise.coroutine(function* () {
|
||||
this._reconnect = false;
|
||||
this._reconnectGenerator = null;
|
||||
if (this._socket) {
|
||||
this._socketClosedDeferred = Zotero.Promise.defer();
|
||||
this._socket.close();
|
||||
return this._socketClosedDeferred.promise;
|
||||
}
|
||||
})
|
||||
};
|
|
@ -713,8 +713,9 @@ Services.scriptloader.loadSubScript("resource://zotero/polyfill.js");
|
|||
yield Zotero.Sync.Data.Local.init();
|
||||
yield Zotero.Sync.Data.Utilities.init();
|
||||
Zotero.Sync.Runner = new Zotero.Sync.Runner_Module;
|
||||
Zotero.Sync.Streamer = new Zotero.Sync.Streamer_Module;
|
||||
Zotero.Sync.EventListeners.init();
|
||||
Zotero.Streamer = new Zotero.Streamer_Module;
|
||||
Zotero.Streamer.init();
|
||||
|
||||
Zotero.MIMETypeHandler.init();
|
||||
yield Zotero.Proxies.init();
|
||||
|
|
|
@ -104,6 +104,7 @@ const xpcomFilesLocal = [
|
|||
'router',
|
||||
'schema',
|
||||
'server',
|
||||
'streamer',
|
||||
'style',
|
||||
'sync',
|
||||
'sync/syncAPIClient',
|
||||
|
@ -113,7 +114,6 @@ const xpcomFilesLocal = [
|
|||
'sync/syncFullTextEngine',
|
||||
'sync/syncLocal',
|
||||
'sync/syncRunner',
|
||||
'sync/syncStreamer',
|
||||
'sync/syncUtilities',
|
||||
'storage',
|
||||
'storage/storageEngine',
|
||||
|
|
|
@ -137,6 +137,9 @@ pref("extensions.zotero.zeroconf.server.enabled", false);
|
|||
// Annotation settings
|
||||
pref("extensions.zotero.annotations.warnOnClose", true);
|
||||
|
||||
// Streaming server
|
||||
pref("extensions.zotero.streaming.enabled", true);
|
||||
|
||||
// Sync
|
||||
pref("extensions.zotero.sync.autoSync", true);
|
||||
pref("extensions.zotero.sync.server.username", '');
|
||||
|
@ -154,7 +157,6 @@ pref("extensions.zotero.sync.storage.groups.enabled", true);
|
|||
pref("extensions.zotero.sync.storage.downloadMode.personal", "on-sync");
|
||||
pref("extensions.zotero.sync.storage.downloadMode.groups", "on-sync");
|
||||
pref("extensions.zotero.sync.fulltext.enabled", true);
|
||||
pref("extensions.zotero.sync.streaming.enabled", true);
|
||||
|
||||
// Proxy
|
||||
pref("extensions.zotero.proxies.autoRecognize", true);
|
||||
|
|
Loading…
Reference in a new issue