commit 86cf7cbd0768cb39a8d62895779f1c80c685f58d
parent b476c7c7c5e7dde9a55cecb693eb297cfd5a0386
Author: Dan Stillman <dstillman@zotero.org>
Date: Tue, 4 Jul 2017 18:03:13 -0400
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.
Diffstat:
8 files changed, 473 insertions(+), 331 deletions(-)
diff --git a/chrome/content/zotero/preferences/preferences_general.js b/chrome/content/zotero/preferences/preferences_general.js
@@ -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) {
diff --git a/chrome/content/zotero/xpcom/schema.js b/chrome/content/zotero/xpcom/schema.js
@@ -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);
- }
- else {
- Zotero.logError(e);
- Zotero.debug(e.status, 1);
- Zotero.debug(e.xmlhttp.responseText, 1);
- Zotero.debug("Error updating from repository " + msg, 1);
+ 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;
}
- // 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']);
- }
-
- return Zotero.Promise.resolve(true);
- })
- .tap(function () {
- updatePDFTools();
});
+
+ Zotero.debug('All translators and styles are up-to-date');
+ if (!force) {
+ _setRepositoryTimer(ZOTERO_CONFIG.REPOSITORY_CHECK_INTERVAL);
+ }
+ updatePDFTools();
+ return true;
}
- 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 });
+ var updated = false;
+ try {
+ for (var i=0, len=translatorUpdates.length; i<len; i++){
+ await _translatorXMLToFile(translatorUpdates[i]);
}
- catch (e) {
- Zotero.debug(e, 1);
- if (!force) {
- _setRepositoryTimer(ZOTERO_CONFIG['REPOSITORY_RETRY_INTERVAL']);
- }
- return false;
+
+ for (var i=0, len=styleUpdates.length; i<len; i++){
+ await _styleXMLToFile(styleUpdates[i]);
}
- return true;
- })
- .then(function (update) {
- if (!update) return false;
+ // Rebuild caches
+ await Zotero.Translators.reinit({ fromSchemaUpdate: force != 1 });
+ await Zotero.Styles.reinit({ fromSchemaUpdate: force != 1 });
- return Zotero.DB.executeTransaction(function* (conn) {
+ 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);
// 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
-
- 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);
+ var displayInterval = delay + fudge;
+ delay = (delay + fudge) * 1000; // convert to ms
+
+ 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;
}
diff --git a/chrome/content/zotero/xpcom/streamer.js b/chrome/content/zotero/xpcom/streamer.js
@@ -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);
+ }
+ }
+};
diff --git a/chrome/content/zotero/xpcom/sync/syncEventListeners.js b/chrome/content/zotero/xpcom/sync/syncEventListeners.js
@@ -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();
}
}
diff --git a/chrome/content/zotero/xpcom/sync/syncStreamer.js b/chrome/content/zotero/xpcom/sync/syncStreamer.js
@@ -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;
- }
- })
-};
diff --git a/chrome/content/zotero/xpcom/zotero.js b/chrome/content/zotero/xpcom/zotero.js
@@ -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();
diff --git a/components/zotero-service.js b/components/zotero-service.js
@@ -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',
diff --git a/defaults/preferences/zotero.js b/defaults/preferences/zotero.js
@@ -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);