Feed syncing (#1044)

Closes #1036

Also:

- Store sync info for feeds more compactly. Address #1037
This commit is contained in:
Adomas Ven 2016-06-22 10:24:22 +03:00 committed by Dan Stillman
parent 451c12513e
commit dd8fd2b1ac
7 changed files with 112 additions and 52 deletions

View File

@ -70,7 +70,6 @@ Zotero.Feed = function(params = {}) {
this._feedUnreadCount = null; this._feedUnreadCount = null;
this._updating = false; this._updating = false;
this._syncedSettings = null;
this._previousURL = null; this._previousURL = null;
} }
@ -301,8 +300,8 @@ Zotero.Feed.prototype._finalizeSave = Zotero.Promise.coroutine(function* (env) {
} }
if (env.isNew || this._previousURL) { if (env.isNew || this._previousURL) {
Zotero.Feeds.register(this); Zotero.Feeds.register(this);
yield this.storeSyncedSettings();
} }
yield this.storeSyncedSettings();
this._previousURL = null; this._previousURL = null;
}); });
@ -335,12 +334,7 @@ Zotero.Feed.prototype.erase = Zotero.Promise.coroutine(function* (options = {})
Zotero.Feed.prototype.storeSyncedSettings = Zotero.Promise.coroutine(function* () { Zotero.Feed.prototype.storeSyncedSettings = Zotero.Promise.coroutine(function* () {
let syncedFeeds = Zotero.SyncedSettings.get(Zotero.Libraries.userLibraryID, 'feeds') || {}; let syncedFeeds = Zotero.SyncedSettings.get(Zotero.Libraries.userLibraryID, 'feeds') || {};
syncedFeeds[this.url] = { syncedFeeds[this.url] = [this.name, this.cleanupAfter, this.refreshInterval];
url: this.url,
name: this.name,
cleanupAfter: this.cleanupAfter,
refreshInterval: this.refreshInterval
};
return Zotero.SyncedSettings.set(Zotero.Libraries.userLibraryID, 'feeds', syncedFeeds); return Zotero.SyncedSettings.set(Zotero.Libraries.userLibraryID, 'feeds', syncedFeeds);
}); });

View File

@ -30,6 +30,18 @@ Zotero.Feeds = new function() {
this.scheduleNextFeedCheck(); this.scheduleNextFeedCheck();
this._timeoutID = null; this._timeoutID = null;
}, 5000); }, 5000);
Zotero.SyncedSettings.onSyncDownload.addListener(Zotero.Libraries.userLibraryID, 'feeds',
(oldValue, newValue, conflict) => {
Zotero.Feeds.restoreFromJSON(newValue, conflict);
}
);
Zotero.Notifier.registerObserver({notify: function(event) {
if (event == 'finish') {
Zotero.Feeds.updateFeeds();
}
}}, ['sync'], 'feedsUpdate');
}; };
this.uninit = function () { this.uninit = function () {
@ -126,37 +138,37 @@ Zotero.Feeds = new function() {
Zotero.debug(json); Zotero.debug(json);
if (merge) { if (merge) {
let syncedFeeds = Zotero.SyncedSettings.get(Zotero.Libraries.userLibraryID, 'feeds'); let syncedFeeds = Zotero.SyncedSettings.get(Zotero.Libraries.userLibraryID, 'feeds');
// Overwrite with remote values for names, etc.
for (let url in json) { for (let url in json) {
if (syncedFeeds[url]) { syncedFeeds[url] = json[url];
syncedFeeds[url].name = json[url].name;
syncedFeeds[url].cleanupAfter = json[url].cleanupAfter;
syncedFeeds[url].refreshInterval = json[url].refreshInterval;
for (let guid in json[url].markedAsRead) {
syncedFeeds[url].markedAsRead[guid] = true;
}
} else {
syncedFeeds[url] = json[url];
}
} }
// But keep all local feeds
json = syncedFeeds; json = syncedFeeds;
} }
json = this._compactifyFeedJSON(json);
yield Zotero.SyncedSettings.set(Zotero.Libraries.userLibraryID, 'feeds', json); yield Zotero.SyncedSettings.set(Zotero.Libraries.userLibraryID, 'feeds', json);
let feeds = Zotero.Feeds.getAll(); let feeds = Zotero.Feeds.getAll();
for (let feed of feeds) { for (let feed of feeds) {
if (json[feed.url]) { if (json[feed.url]) {
Zotero.debug("Feed " + feed.url + " exists remotely and locally"); Zotero.debug("Feed " + feed.url + " exists remotely and locally");
delete json[feed.url]; feed.name = json[feed.url][0];
feed.cleanupAfter = json[feed.url][1];
feed.refreshInterval = json[feed.url][2];
} else { } else {
Zotero.debug("Feed " + feed.url + " does not exist in remote JSON. Deleting"); Zotero.debug("Feed " + feed.url + " does not exist in remote JSON. Deleting");
yield feed.eraseTx(); yield feed.erase();
} }
} }
// Because existing json[feed.url] got deleted, `json` now only contains new feeds // Because existing json[feed.url] got deleted, `json` now only contains new feeds
for (let url in json) { for (let url in json) {
Zotero.debug("Feed " + url + " exists remotely but not locally. Creating"); Zotero.debug("Feed " + url + " exists remotely but not locally. Creating");
let feed = new Zotero.Feed(json[url]); let feed = new Zotero.Feed({
yield feed.saveTx(); url,
yield feed.updateFeed(); name: json[url][0],
cleanupAfter: json[url][1],
refreshInterval: json[url[2]]
});
yield feed.save();
} }
}); });
@ -259,4 +271,16 @@ Zotero.Feeds = new function() {
Zotero.debug("All feed updates done"); Zotero.debug("All feed updates done");
this.scheduleNextFeedCheck(); this.scheduleNextFeedCheck();
}); });
// Conversion from expansive to compact format sync json
// TODO: Remove after beta
this._compactifyFeedJSON = function(json) {
for (let url in json) {
if(Array.isArray(json[url])) {
continue;
}
json[url] = [json[url].name, json[url].cleanupAfter, json[url].refreshInterval];
}
return json;
};
} }

View File

@ -30,12 +30,19 @@ Zotero.Notifier = new function(){
var _types = [ var _types = [
'collection', 'search', 'share', 'share-items', 'item', 'file', 'collection', 'search', 'share', 'share-items', 'item', 'file',
'collection-item', 'item-tag', 'tag', 'setting', 'group', 'trash', 'publications', 'collection-item', 'item-tag', 'tag', 'setting', 'group', 'trash', 'publications',
'bucket', 'relation', 'feed', 'feedItem' 'bucket', 'relation', 'feed', 'feedItem', 'sync'
]; ];
var _inTransaction; var _inTransaction;
var _queue = {}; var _queue = {};
/**
* @param ref {Object} - signature {notify: function(event, type, ids, extraData) {}}
* @param types {Array} - a list of types of events observer should be triggered on
* @param id {String} - an id of the observer used in debug output
* @param priority {Integer} - lower numbers correspond to higher priority of observer execution
* @returns {string}
*/
this.registerObserver = function (ref, types, id, priority) { this.registerObserver = function (ref, types, id, priority) {
if (types){ if (types){
types = Zotero.flattenArguments(types); types = Zotero.flattenArguments(types);

View File

@ -149,7 +149,7 @@ Zotero.Sync.Runner_Module = function (options = {}) {
firstInSession: _firstInSession firstInSession: _firstInSession
}; };
let librariesToSync = options.libraries = yield this.checkLibraries( var librariesToSync = options.libraries = yield this.checkLibraries(
client, client,
options, options,
keyInfo, keyInfo,
@ -223,6 +223,7 @@ Zotero.Sync.Runner_Module = function (options = {}) {
} }
Zotero.debug("Done syncing"); Zotero.debug("Done syncing");
Zotero.Notifier.trigger('finish', 'sync', librariesToSync);
} }
}); });

View File

@ -36,6 +36,40 @@ Zotero.SyncedSettings = (function () {
idColumn: "setting", idColumn: "setting",
table: "syncedSettings", table: "syncedSettings",
/**
* An event which allows to tap into the sync transaction and update
* parts of the client which rely on synced settings.
*/
onSyncDownload: {
listeners: {},
addListener: function(libraryID, setting, fn, bindTarget=null) {
if (!this.listeners[libraryID]) {
this.listeners[libraryID] = {};
}
if (!this.listeners[libraryID][setting]) {
this.listeners[libraryID][setting] = [];
}
this.listeners[libraryID][setting].push([fn, bindTarget]);
},
/**
* @param {Integer} libraryID
* @param {String} setting - name of the setting
* @param {Object} oldValue
* @param {Object} newValue
* @param {Boolean} conflict - true if both client and remote values had changed before sync
*/
trigger: Zotero.Promise.coroutine(function* (libraryID, setting, oldValue, newValue, conflict) {
var libListeners = this.listeners[libraryID] || {};
var settingListeners = libListeners[setting] || [];
Array.prototype.splice.call(arguments, 0, 2);
if (settingListeners) {
for (let listener of settingListeners) {
yield Zotero.Promise.resolve(listener[0].apply(listener[1], arguments));
}
}
})
},
loadAll: Zotero.Promise.coroutine(function* (libraryID) { loadAll: Zotero.Promise.coroutine(function* (libraryID) {
Zotero.debug("Loading synced settings for library " + libraryID); Zotero.debug("Loading synced settings for library " + libraryID);
@ -175,11 +209,13 @@ Zotero.SyncedSettings = (function () {
version = parseInt(version); version = parseInt(version);
if (hasCurrentValue) { if (hasCurrentValue) {
var sql = "UPDATE syncedSettings SET value=?, version=?, synced=? " var sql = "UPDATE syncedSettings SET " + (version > 0 ? "version=?, " : "") +
+ "WHERE setting=? AND libraryID=?"; "value=?, synced=? WHERE setting=? AND libraryID=?";
yield Zotero.DB.queryAsync( var args = [JSON.stringify(value), synced, setting, libraryID];
sql, [JSON.stringify(value), version, synced, setting, libraryID] if (version > 0) {
); args.unshift(version)
}
yield Zotero.DB.queryAsync(sql, args);
} }
else { else {
var sql = "INSERT INTO syncedSettings " var sql = "INSERT INTO syncedSettings "
@ -189,12 +225,18 @@ Zotero.SyncedSettings = (function () {
); );
} }
var metadata = this.getMetadata(libraryID, setting);
_cache[libraryID][setting] = { _cache[libraryID][setting] = {
value, value,
synced: !!synced, synced: !!synced,
version version: version > 0 ? version : metadata.version
} };
var conflict = metadata && !metadata.synced && metadata.version < version;
if (version > 0) {
yield this.onSyncDownload.trigger(libraryID, setting, currentValue, value, conflict);
}
yield Zotero.Notifier.trigger(event, 'setting', [id], extraData); yield Zotero.Notifier.trigger(event, 'setting', [id], extraData);
return true; return true;
}), }),

View File

@ -200,17 +200,18 @@ describe("Zotero.Feed", function() {
}); });
describe("#storeSyncedSettings", function() { describe("#storeSyncedSettings", function() {
it("should store updated settings for the feed", function* () { it("should store settings for feed in compact format", function* () {
let settings = { let url = 'http://' + Zotero.Utilities.randomString().toLowerCase() + '.com/feed.rss';
name: Zotero.Utilities.randomString(), let settings = [Zotero.Utilities.randomString(), 1, 1];
url: 'http://' + Zotero.Utilities.randomString().toLowerCase() + '.com/feed.rss', let feed = yield createFeed({
refreshInterval: 1, url,
cleanupAfter: 1 name: settings[0],
}; cleanupAfter: settings[1],
let feed = yield createFeed(settings); refreshInterval: settings[2]
});
let syncedFeeds = Zotero.SyncedSettings.get(Zotero.Libraries.userLibraryID, 'feeds'); let syncedFeeds = Zotero.SyncedSettings.get(Zotero.Libraries.userLibraryID, 'feeds');
assert.deepEqual(syncedFeeds[feed.url], settings); assert.deepEqual(syncedFeeds[url], settings);
}); });
}); });

View File

@ -47,14 +47,6 @@ describe("Zotero.Feeds", function () {
var json = {}; var json = {};
var expiredFeedURL, existingFeedURL; var expiredFeedURL, existingFeedURL;
before(function() {
sinon.stub(Zotero.Feed.prototype, 'updateFeed').resolves();
});
after(function() {
Zotero.Feed.prototype.updateFeed.restore();
});
beforeEach(function* () { beforeEach(function* () {
yield clearFeeds(); yield clearFeeds();
@ -65,7 +57,6 @@ describe("Zotero.Feeds", function () {
name: Zotero.Utilities.randomString(), name: Zotero.Utilities.randomString(),
refreshInterval: 5, refreshInterval: 5,
cleanupAfter: 3, cleanupAfter: 3,
markedAsRead: []
}; };
if (i == 0) { if (i == 0) {
existingFeedURL = url; existingFeedURL = url;