All files / src/init/sa ve.init.sa.SafeStorage.js

43.02% Statements 37/86
16.66% Branches 4/24
50% Functions 10/20
42.35% Lines 36/85

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 2091x   1x                           1x           1x                       1x 16x     16x 16x 16x             1x                         1x                             1x                       1x                                 1x                       1x                               1x         1x 16x 16x 16x 16x 16x           16x       16x                   1x 16x 16x 16x 16x 16x 16x 16x 16x                     16x                 16x               1x                    
( function () {
	// Copied from mediawiki.requestIdleCallback
	var requestIdleCallbackInternal = function ( callback ) {
		setTimeout( function () {
			var start = ve.now();
			callback( {
				didTimeout: false,
				timeRemaining: function () {
					// Hard code a target maximum busy time of 50 milliseconds
					return Math.max( 0, 50 - ( ve.now() - start ) );
				}
			} );
		}, 1 );
	};
 
	// eslint-disable-next-line compat/compat
	var requestIdleCallback = window.requestIdleCallback ?
		// Bind because it throws TypeError if context is not window
		// eslint-disable-next-line compat/compat
		window.requestIdleCallback.bind( window ) :
		requestIdleCallbackInternal;
 
	var EXPIRY_PREFIX = '_EXPIRY_';
	/**
	 * Implementation of ve.init.SafeStorage
	 *
	 * Duplicate of mediawiki.storage.
	 *
	 * @class ve.init.sa.SafeStorage
	 * @extends ve.init.SafeStorage
	 *
	 * @constructor
	 * @param {Storage|undefined} store The Storage instance to wrap around
	 */
	ve.init.sa.SafeStorage = function ( store ) {
		this.store = store;
 
		// Purge expired items once per page session
		var storage = this;
		setTimeout( function () {
			storage.clearExpired();
		}, 2000 );
	};
 
	/**
	 * @inheritdoc
	 */
	ve.init.sa.SafeStorage.prototype.get = function ( key ) {
		if ( this.isExpired( key ) ) {
			return null;
		}
		try {
			return this.store.getItem( key );
		} catch ( e ) {}
		return false;
	};
 
	/**
	 * @inheritdoc
	 */
	ve.init.sa.SafeStorage.prototype.set = function ( key, value, expiry ) {
		if ( key.slice( 0, EXPIRY_PREFIX.length ) === EXPIRY_PREFIX ) {
			throw new Error( 'Key can\'t have a prefix of ' + EXPIRY_PREFIX );
		}
		try {
			this.store.setItem( key, value );
			this.setExpires( key, expiry );
			return true;
		} catch ( e ) {}
		return false;
	};
 
	/**
	 * @inheritdoc
	 */
	ve.init.sa.SafeStorage.prototype.remove = function ( key ) {
		try {
			this.store.removeItem( key );
			this.setExpires( key );
			return true;
		} catch ( e ) {}
		return false;
	};
 
	/**
	 * @inheritdoc
	 */
	ve.init.sa.SafeStorage.prototype.getObject = function ( key ) {
		var json = this.get( key );
 
		if ( json === false ) {
			return false;
		}
 
		try {
			return JSON.parse( json );
		} catch ( e ) {}
 
		return null;
	};
 
	/**
	 * @inheritdoc
	 */
	ve.init.sa.SafeStorage.prototype.setObject = function ( key, value, expiry ) {
		var json;
		try {
			json = JSON.stringify( value );
			return this.set( key, json, expiry );
		} catch ( e ) {}
		return false;
	};
 
	/**
	 * @inheritdoc
	 */
	ve.init.sa.SafeStorage.prototype.setExpires = function ( key, expiry ) {
		if ( expiry ) {
			try {
				this.store.setItem(
					EXPIRY_PREFIX + key,
					Math.floor( Date.now() / 1000 ) + expiry
				);
			} catch ( e ) {}
		} else {
			try {
				this.store.removeItem( EXPIRY_PREFIX + key );
			} catch ( e ) {}
		}
	};
 
	// Minimum amount of time (in milliseconds) for an iteration involving localStorage access.
	var MIN_WORK_TIME = 3;
 
	/**
	 * @inheritdoc
	 */
	ve.init.sa.SafeStorage.prototype.clearExpired = function () {
		var storage = this;
		return this.getExpiryKeys().then( function ( keys ) {
			return $.Deferred( function ( d ) {
				requestIdleCallback( function iterate( deadline ) {
					while ( keys[ 0 ] !== undefined && deadline.timeRemaining() > MIN_WORK_TIME ) {
						var key = keys.shift();
						if ( storage.isExpired( key ) ) {
							storage.remove( key );
						}
					}
					Iif ( keys[ 0 ] !== undefined ) {
						// Ran out of time with keys still to remove, continue later
						requestIdleCallback( iterate );
					} else {
						return d.resolve();
					}
				} );
			} );
		} );
	};
 
	/**
	 * @inheritdoc
	 */
	ve.init.sa.SafeStorage.prototype.getExpiryKeys = function () {
		var store = this.store;
		return $.Deferred( function ( d ) {
			requestIdleCallback( function ( deadline ) {
				var prefixLength = EXPIRY_PREFIX.length;
				var keys = [];
				var length = 0;
				try {
					length = store.length;
				} catch ( e ) {}
 
				// Optimization: If time runs out, degrade to checking fewer keys.
				// We will get another chance during a future page view. Iterate forward
				// so that older keys are checked first and increase likelihood of recovering
				// from key exhaustion.
				//
				// We don't expect to have more keys than we can handle in 50ms long-task window.
				// But, we might still run out of time when other tasks run before this,
				// or when the device receives UI events (especially on low-end devices).
				for ( var i = 0; ( i < length && deadline.timeRemaining() > MIN_WORK_TIME ); i++ ) {
					var key = null;
					try {
						key = store.key( i );
					} catch ( e ) {}
					if ( key !== null && key.slice( 0, prefixLength ) === EXPIRY_PREFIX ) {
						keys.push( key.slice( prefixLength ) );
					}
				}
				d.resolve( keys );
			} );
		} ).promise();
	};
 
	/**
	 * @inheritdoc
	 */
	ve.init.sa.SafeStorage.prototype.isExpired = function ( key ) {
		var expiry;
		try {
			expiry = this.store.getItem( EXPIRY_PREFIX + key );
		} catch ( e ) {
			return false;
		}
		return !!expiry && expiry < Math.floor( Date.now() / 1000 );
	};
}() );