Push-based sync triggering
Immediate sync triggering on remote library change using WebSocket API. Currently kicks off a normal sync process for the modified library -- actual object data isn't pushed. (This might not stay enabled for 5.0 Final.)
This commit is contained in:
parent
7fd3a8c5d1
commit
2beb2c514c
11 changed files with 224 additions and 4 deletions
|
@ -30,7 +30,7 @@ Zotero.Notifier = new function(){
|
|||
var _types = [
|
||||
'collection', 'search', 'share', 'share-items', 'item', 'file',
|
||||
'collection-item', 'item-tag', 'tag', 'setting', 'group', 'trash', 'publications',
|
||||
'bucket', 'relation', 'feed', 'feedItem', 'sync'
|
||||
'bucket', 'relation', 'feed', 'feedItem', 'sync', 'api-key'
|
||||
];
|
||||
var _inTransaction;
|
||||
var _queue = {};
|
||||
|
|
|
@ -111,6 +111,7 @@ Zotero.Sync.EventListeners.AutoSyncListener = {
|
|||
|
||||
register: function () {
|
||||
this._observerID = Zotero.Notifier.registerObserver(this, false, 'autosync');
|
||||
Zotero.Sync.Streamer.init();
|
||||
},
|
||||
|
||||
notify: function (event, type, ids, extraData) {
|
||||
|
@ -163,6 +164,7 @@ Zotero.Sync.EventListeners.AutoSyncListener = {
|
|||
if (this._observerID) {
|
||||
Zotero.Notifier.unregisterObserver(this._observerID);
|
||||
}
|
||||
Zotero.Sync.Streamer.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -80,6 +80,7 @@ Zotero.Sync.Data.Local = {
|
|||
Zotero.debug("Clearing old API key");
|
||||
loginManager.removeLogin(oldLoginInfo);
|
||||
}
|
||||
Zotero.Notifier.trigger('delete', 'api-key', []);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -102,6 +103,7 @@ Zotero.Sync.Data.Local = {
|
|||
Zotero.debug("Replacing API key");
|
||||
loginManager.modifyLogin(oldLoginInfo, loginInfo);
|
||||
}
|
||||
Zotero.Notifier.trigger('modify', 'api-key', []);
|
||||
},
|
||||
|
||||
|
||||
|
|
|
@ -97,7 +97,7 @@ Zotero.Sync.Runner_Module = function (options = {}) {
|
|||
* @param {Function} [options.onError] Function to pass errors to instead of
|
||||
* handling internally (used for testing)
|
||||
*/
|
||||
this.sync = Zotero.Promise.coroutine(function* (options = {}) {
|
||||
this.sync = Zotero.serial(Zotero.Promise.coroutine(function* (options = {}) {
|
||||
// Clear message list
|
||||
_errors = [];
|
||||
|
||||
|
@ -240,7 +240,7 @@ Zotero.Sync.Runner_Module = function (options = {}) {
|
|||
Zotero.debug("Done syncing");
|
||||
Zotero.Notifier.trigger('finish', 'sync', librariesToSync || []);
|
||||
}
|
||||
});
|
||||
}));
|
||||
|
||||
|
||||
/**
|
||||
|
|
183
chrome/content/zotero/xpcom/sync/syncStreamer.js
Normal file
183
chrome/content/zotero/xpcom/sync/syncStreamer.js
Normal file
|
@ -0,0 +1,183 @@
|
|||
/*
|
||||
***** 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* () {
|
||||
// Connect to the streaming server
|
||||
if (!Zotero.Prefs.get('sync.autoSync') || !Zotero.Prefs.get('sync.streaming.enabled')) {
|
||||
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);
|
||||
}
|
||||
}),
|
||||
|
||||
_connect: function (url, apiKey) {
|
||||
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._reconnectGenerator = null;
|
||||
};
|
||||
|
||||
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") {
|
||||
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') {
|
||||
yield Zotero.Sync.Runner.sync({
|
||||
background: true
|
||||
});
|
||||
}
|
||||
// Library modified
|
||||
else if (data.event == 'topicUpdated') {
|
||||
let library = Zotero.URI.getPathLibrary(data.topic);
|
||||
if (library) {
|
||||
// Ignore if skipped library
|
||||
let skipped = Zotero.Sync.Data.Local.getSkippedLibraries();
|
||||
if (skipped.includes(library.id)) return;
|
||||
|
||||
yield Zotero.Sync.Runner.sync({
|
||||
background: true,
|
||||
libraries: [library.id]
|
||||
});
|
||||
}
|
||||
}
|
||||
}.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 >= 4000) {
|
||||
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;
|
||||
}
|
||||
})
|
||||
};
|
|
@ -117,6 +117,34 @@ Zotero.URI = new function () {
|
|||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get library from path (e.g., users/6 or groups/1)
|
||||
*
|
||||
* @return {Zotero.Library|false}
|
||||
*/
|
||||
this.getPathLibrary = function (path) {
|
||||
let matches = path.match(/^\/\/?users\/(\d+)(\/publications)?/);
|
||||
if (matches) {
|
||||
let userID = matches[1];
|
||||
let currentUserID = Zotero.Users.getCurrentUserID();
|
||||
if (userID != currentUserID) {
|
||||
Zotero.debug("User ID from streaming server doesn't match current id! "
|
||||
+ `(${userID} != ${currentUserID})`);
|
||||
return false;
|
||||
}
|
||||
if (matches[2]) {
|
||||
return Zotero.Libraries.get(Zotero.Libraries.publicationsLibraryID);
|
||||
}
|
||||
return Zotero.Libraries.userLibrary;
|
||||
}
|
||||
matches = event.data.topic.match(/^\/groups\/(\d+)/);
|
||||
if (matches) {
|
||||
let groupID = matches[1];
|
||||
return Zotero.Groups.get(groupID);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Return URI of item, which might be a local URI if user hasn't synced
|
||||
*/
|
||||
|
|
|
@ -619,6 +619,7 @@ Zotero.Utilities.Internal = {
|
|||
* maxTime isn't specified, the promises will yield true.
|
||||
*/
|
||||
"delayGenerator": function* (intervals, maxTime) {
|
||||
var delay;
|
||||
var totalTime = 0;
|
||||
var last = false;
|
||||
while (true) {
|
||||
|
|
|
@ -707,8 +707,9 @@ Services.scriptloader.loadSubScript("resource://zotero/polyfill.js");
|
|||
|
||||
yield Zotero.Sync.Data.Local.init();
|
||||
yield Zotero.Sync.Data.Utilities.init();
|
||||
Zotero.Sync.EventListeners.init();
|
||||
Zotero.Sync.Runner = new Zotero.Sync.Runner_Module;
|
||||
Zotero.Sync.Streamer = new Zotero.Sync.Streamer_Module;
|
||||
Zotero.Sync.EventListeners.init();
|
||||
|
||||
Zotero.MIMETypeHandler.init();
|
||||
yield Zotero.Proxies.init();
|
||||
|
|
|
@ -113,6 +113,7 @@ const xpcomFilesLocal = [
|
|||
'sync/syncFullTextEngine',
|
||||
'sync/syncLocal',
|
||||
'sync/syncRunner',
|
||||
'sync/syncStreamer',
|
||||
'sync/syncUtilities',
|
||||
'storage',
|
||||
'storage/storageEngine',
|
||||
|
|
|
@ -156,6 +156,7 @@ 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);
|
||||
|
|
|
@ -10,6 +10,7 @@ var ZOTERO_CONFIG = {
|
|||
WWW_BASE_URL: 'https://www.zotero.org/',
|
||||
PROXY_AUTH_URL: 'https://s3.amazonaws.com/zotero.org/proxy-auth',
|
||||
API_URL: 'https://api.zotero.org/',
|
||||
STREAMING_URL: 'wss://stream.zotero.org/',
|
||||
API_VERSION: 3,
|
||||
PREF_BRANCH: 'extensions.zotero.',
|
||||
BOOKMARKLET_ORIGIN: 'https://www.zotero.org',
|
||||
|
|
Loading…
Reference in a new issue