syncedSettings.js (8097B)
1 /* 2 ***** BEGIN LICENSE BLOCK ***** 3 4 Copyright © 2013 Center for History and New Media 5 George Mason University, Fairfax, Virginia, USA 6 http://zotero.org 7 8 This file is part of Zotero. 9 10 Zotero is free software: you can redistribute it and/or modify 11 it under the terms of the GNU Affero General Public License as published by 12 the Free Software Foundation, either version 3 of the License, or 13 (at your option) any later version. 14 15 Zotero is distributed in the hope that it will be useful, 16 but WITHOUT ANY WARRANTY; without even the implied warranty of 17 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 18 GNU Affero General Public License for more details. 19 20 You should have received a copy of the GNU Affero General Public License 21 along with Zotero. If not, see <http://www.gnu.org/licenses/>. 22 23 ***** END LICENSE BLOCK ***** 24 */ 25 26 /** 27 * @namespace 28 */ 29 Zotero.SyncedSettings = (function () { 30 var _cache = {}; 31 32 // 33 // Public methods 34 // 35 var module = { 36 idColumn: "setting", 37 table: "syncedSettings", 38 39 /** 40 * An event which allows to tap into the sync transaction and update 41 * parts of the client which rely on synced settings. 42 */ 43 onSyncDownload: { 44 listeners: {}, 45 addListener: function(libraryID, setting, fn, bindTarget=null) { 46 if (!this.listeners[libraryID]) { 47 this.listeners[libraryID] = {}; 48 } 49 if (!this.listeners[libraryID][setting]) { 50 this.listeners[libraryID][setting] = []; 51 } 52 this.listeners[libraryID][setting].push([fn, bindTarget]); 53 }, 54 /** 55 * @param {Integer} libraryID 56 * @param {String} setting - name of the setting 57 * @param {Object} oldValue 58 * @param {Object} newValue 59 * @param {Boolean} conflict - true if both client and remote values had changed before sync 60 */ 61 trigger: Zotero.Promise.coroutine(function* (libraryID, setting, oldValue, newValue, conflict) { 62 var libListeners = this.listeners[libraryID] || {}; 63 var settingListeners = libListeners[setting] || []; 64 Array.prototype.splice.call(arguments, 0, 2); 65 if (settingListeners) { 66 for (let listener of settingListeners) { 67 yield Zotero.Promise.resolve(listener[0].apply(listener[1], arguments)); 68 } 69 } 70 }) 71 }, 72 73 loadAll: Zotero.Promise.coroutine(function* (libraryID) { 74 Zotero.debug("Loading synced settings for library " + libraryID); 75 76 if (!_cache[libraryID]) { 77 _cache[libraryID] = {}; 78 } 79 80 var invalid = []; 81 82 var sql = "SELECT setting, value, synced, version FROM syncedSettings " 83 + "WHERE libraryID=?"; 84 yield Zotero.DB.queryAsync( 85 sql, 86 libraryID, 87 { 88 onRow: function (row) { 89 var setting = row.getResultByIndex(0); 90 91 var value = row.getResultByIndex(1); 92 try { 93 value = JSON.parse(value); 94 } 95 catch (e) { 96 invalid.push([libraryID, setting]); 97 return; 98 } 99 100 _cache[libraryID][setting] = { 101 value, 102 synced: !!row.getResultByIndex(2), 103 version: row.getResultByIndex(3) 104 }; 105 } 106 } 107 ); 108 109 // TODO: Delete invalid settings 110 }), 111 112 /** 113 * Return settings object 114 * 115 * @return {Object|null} 116 */ 117 get: function (libraryID, setting) { 118 if (!_cache[libraryID]) { 119 throw new Zotero.Exception.UnloadedDataException( 120 "Synced settings not loaded for library " + libraryID, 121 "syncedSettings" 122 ); 123 } 124 125 if (!_cache[libraryID][setting]) { 126 return null; 127 } 128 129 return JSON.parse(JSON.stringify(_cache[libraryID][setting].value)); 130 }, 131 132 /** 133 * Used by sync and tests 134 * 135 * @return {Object} - Object with 'synced' and 'version' properties 136 */ 137 getMetadata: function (libraryID, setting) { 138 if (!_cache[libraryID]) { 139 throw new Zotero.Exception.UnloadedDataException( 140 "Synced settings not loaded for library " + libraryID, 141 "syncedSettings" 142 ); 143 } 144 145 var o = _cache[libraryID][setting]; 146 if (!o) { 147 return null; 148 } 149 return { 150 synced: o.synced, 151 version: o.version 152 }; 153 }, 154 155 getUnsynced: Zotero.Promise.coroutine(function* (libraryID) { 156 var sql = "SELECT setting, value FROM syncedSettings WHERE synced=0 AND libraryID=?"; 157 var rows = yield Zotero.DB.queryAsync(sql, libraryID); 158 var obj = {}; 159 rows.forEach(row => obj[row.setting] = JSON.parse(row.value)); 160 return obj; 161 }), 162 163 markAsSynced: Zotero.Promise.coroutine(function* (libraryID, settings, version) { 164 var sql = "UPDATE syncedSettings SET synced=1, version=? WHERE libraryID=? AND setting IN " 165 + "(" + settings.map(x => '?').join(', ') + ")"; 166 yield Zotero.DB.queryAsync(sql, [version, libraryID].concat(settings)); 167 for (let key of settings) { 168 let setting = _cache[libraryID][key]; 169 setting.synced = true; 170 setting.version = version; 171 } 172 }), 173 174 /** 175 * Used for restore-to-server 176 */ 177 markAllAsUnsynced: async function (libraryID) { 178 var sql = "UPDATE syncedSettings SET synced=0, version=0 WHERE libraryID=?"; 179 await Zotero.DB.queryAsync(sql, libraryID); 180 for (let key in _cache[libraryID]) { 181 let setting = _cache[libraryID][key]; 182 setting.synced = false; 183 setting.version = 0; 184 } 185 }, 186 187 set: Zotero.Promise.coroutine(function* (libraryID, setting, value, version = 0, synced) { 188 if (typeof value == undefined) { 189 throw new Error("Value not provided"); 190 } 191 192 // Prevents a whole bunch of headache if you continue modifying the object after calling #set() 193 if (value instanceof Array) { 194 value = Array.from(value); 195 } 196 else if (typeof value == 'object') { 197 value = Object.assign({}, value); 198 } 199 200 var currentValue = this.get(libraryID, setting); 201 var hasCurrentValue = currentValue !== null; 202 203 // Value hasn't changed 204 if (value === currentValue) { 205 return false; 206 } 207 208 var id = libraryID + '/' + setting; 209 210 if (hasCurrentValue) { 211 var extraData = {}; 212 extraData[id] = { 213 changed: {} 214 }; 215 extraData[id].changed = { 216 value: currentValue 217 }; 218 } 219 220 if (!hasCurrentValue) { 221 var event = 'add'; 222 var extraData = {}; 223 } 224 else { 225 var event = 'modify'; 226 } 227 228 synced = synced ? 1 : 0; 229 version = parseInt(version); 230 231 if (hasCurrentValue) { 232 var sql = "UPDATE syncedSettings SET " + (version > 0 ? "version=?, " : "") + 233 "value=?, synced=? WHERE setting=? AND libraryID=?"; 234 var args = [JSON.stringify(value), synced, setting, libraryID]; 235 if (version > 0) { 236 args.unshift(version) 237 } 238 yield Zotero.DB.queryAsync(sql, args); 239 } 240 else { 241 var sql = "INSERT INTO syncedSettings " 242 + "(setting, libraryID, value, version, synced) VALUES (?, ?, ?, ?, ?)"; 243 yield Zotero.DB.queryAsync( 244 sql, [setting, libraryID, JSON.stringify(value), version, synced] 245 ); 246 } 247 248 var metadata = this.getMetadata(libraryID, setting); 249 250 _cache[libraryID][setting] = { 251 value, 252 synced: !!synced, 253 version: version > 0 || !hasCurrentValue ? version : metadata.version 254 }; 255 256 var conflict = metadata && !metadata.synced && metadata.version < version; 257 if (version > 0) { 258 yield this.onSyncDownload.trigger(libraryID, setting, currentValue, value, conflict); 259 } 260 yield Zotero.Notifier.trigger(event, 'setting', [id], extraData); 261 return true; 262 }), 263 264 clear: Zotero.Promise.coroutine(function* (libraryID, setting, options) { 265 options = options || {}; 266 267 var currentValue = this.get(libraryID, setting); 268 var hasCurrentValue = currentValue !== null; 269 270 var id = libraryID + '/' + setting; 271 272 var extraData = {}; 273 extraData[id] = { 274 changed: { 275 value: currentValue 276 } 277 }; 278 if (options.skipDeleteLog) { 279 extraData[id].skipDeleteLog = true; 280 } 281 282 var sql = "DELETE FROM syncedSettings WHERE setting=? AND libraryID=?"; 283 yield Zotero.DB.queryAsync(sql, [setting, libraryID]); 284 285 delete _cache[libraryID][setting]; 286 287 yield Zotero.Notifier.trigger('delete', 'setting', [id], extraData); 288 return true; 289 }) 290 }; 291 292 return module; 293 }());