diff --git a/chrome/content/zotero/preferences/preferences_general.js b/chrome/content/zotero/preferences/preferences_general.js index 10bd9079f..2cbb081a6 100644 --- 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 index d65addadf..d451fc1e0 100644 --- 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); + 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 Zotero.Schema.updateFromRepository(), delay); + _nextRepositoryUpdate = Date.now() + delay; } diff --git a/chrome/content/zotero/xpcom/streamer.js b/chrome/content/zotero/xpcom/streamer.js new file mode 100644 index 000000000..00a65f80a --- /dev/null +++ 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 . + + ***** 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 index 377a421ba..f6a7d6846 100644 --- 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 deleted file mode 100644 index 1f993ce86..000000000 --- a/chrome/content/zotero/xpcom/sync/syncStreamer.js +++ /dev/null @@ -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 . - - ***** 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 index cab85ba3d..0a2b705a4 100644 --- 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 index 175d427b7..917edca11 100644 --- 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 index a083556ef..6fcc0fd8d 100644 --- 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);