www

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | Submodules | README | LICENSE

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 }());