zotero/chrome/content/zotero/xpcom/streamer.js
Dan Stillman 7b9bad1d52 Don't sync group on push notification if not selected for syncing
If a group library was deselected in the "Choose Libraries…" window, a
remote change would still trigger a sync if auto-sync was enabled. (My
Library was not affected.)
2021-01-09 14:58:51 -05:00

296 lines
8.1 KiB
JavaScript

/*
***** 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: (event, type) => {
if (event == 'modify' || event == 'delete') {
this._update();
}
}
};
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') && Zotero.Sync.Runner.enabled) {
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.Sync.Runner.enabled)
|| 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
if (!Zotero.Sync.Data.Local.filterSkippedLibraries([library]).length) {
Zotero.debug("Library not selected for syncing -- skipping");
return;
}
if (data.version && data.version == library.libraryVersion) {
Zotero.debug("Library is already up to date");
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);
}
}
};