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:
Dan Stillman 2017-07-04 18:03:13 -04:00
parent b476c7c7c5
commit 86cf7cbd07
8 changed files with 481 additions and 339 deletions

View file

@ -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) {

View file

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

View 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);
}
}
};

View file

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

View file

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

View file

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

View file

@ -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',

View file

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