2
0
Fork 0
es5-ff-spa-loader/es5-ff-spa-loader.js

816 lines
25 KiB
JavaScript

/*
* Copyright (c) 2015-2016, Willem Cazander
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification, are permitted provided
* that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this list of conditions and the
* following disclaimer.
* * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and
* the following disclaimer in the documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
* THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
* OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
* TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
/* jslint browser: true */
/* global angular,define */
/**
* FFSpaLoader is an assets loader for single page applications.
* Its build around the concept the there is only a single static index.html which
* synces all its assets to local cache for offline and or fast page loads.
*
* @module FFSpaLoader
*/
(function (root, factory) {
if ( typeof define === 'function' && define.amd ) {
define('FFSpaLoader', factory(root));
} else if ( typeof exports === 'object' ) {
module.exports = factory(root);
} else {
root.FFSpaLoader = factory(root);
}
})(this || window, /** @lends module:FFSpaLoader */ function (rootWindow) {
'use strict';
var options = {
debug: {
enable: false,
handler: null, // auto filled
prefix: 'FFSpaLoader.'
},
error: {
handler: null, // auto filled
title: 'Loader ',
style: '.ffError { margin: auto;width: 90%;border: 3px solid red;padding-left: 1em;padding-bottom: 1em;}'
},
boot: {
cordova: {
enable: true,
timeout: -1,
flag: 'FFCordovaDevice'
},
angular: {
enable: true,
modules: []
},
},
server: {
url: null,
assets: null,
timeout: 4096,
flag: 'FFServerUrl',
header: {
request: { // TODO: add header support
'X-FFSpaLoader': '42'
},
response: {
}
},
question: {
transport: 'http://',
title: 'Server',
text: 'Please provide the server name;', // TODO: rename .ffAskUrl
style: '.ffAskUrl { font-size: 1em;margin: auto;width: 90%;border: 3px solid #73AD21;padding-left: 1em;padding-bottom: 1em;} .ffAskUrl > div {font-size: 0.8em;color: #ccc;} .ffAskUrl > div > * {} .ffAskUrl > div > input {} .ffAskUrlError{ color: red}',
}
},
cache: {
meta: null,
js: null,
css: null,
cssData: null
}
};
var cacheDB = null; // single instance for websql
var factory = {
detect: {
localStorage: function() {
try {
var testData = 'localStorageDetect';
rootWindow.localStorage.setItem(testData, testData); // throws err in private browsing mode
rootWindow.localStorage.removeItem(testData);
return true;
} catch(e) {
return false;
}
},
openDatabase: function() {
return 'openDatabase' in rootWindow;
},
sqlitePlugin: function() {
return 'sqlitePlugin' in rootWindow;
},
cordova: function() {
return 'cordova' in rootWindow;
},
cordovaDevice: function() {
return options.boot.cordova.flag in rootWindow;
}
},
cache: {
localStorage: function() {
return {
cacheGetValue: function(key, cb) {
try {
var dataRaw = rootWindow.localStorage.getItem(key);
var data = JSON.parse(dataRaw);
cb(null, data);
} catch(e) {
cb(e);
}
},
cacheSetValue: function(key, value, cb) {
try {
rootWindow.localStorage.setItem(key,JSON.stringify(value));
cb(null);
} catch(e) {
cb(e);
}
},
cacheDeleteValue: function(key, cb) {
try {
rootWindow.localStorage.removeItem(key);
cb(null);
} catch(e) {
cb(e);
}
}
};
},
websql: function(opt) {
if (opt === undefined) { opt = {}; }
if (opt.name === undefined) { opt.name = 'ffSpaLoader.db'; }
if (opt.size === undefined) { opt.size = -1; }
if (opt.version === undefined) { opt.version = '1.0'; }
if (opt.openDatabase === undefined) { opt.openDatabase = rootWindow.openDatabase; }
var nullDataHandler = function(cb) {
return function () {
cb(null);
};
};
var executeSql = function(tx, query, values, resultHandler, errorHandler) {
tx.executeSql(query, values, resultHandler, function(tx,err) {
errorHandler(new Error('Code: '+err.code+' '+err.message+' by query: '+query));
});
};
var cacheDBInit = function(cb) {
cacheDB.transaction(function(tx) {
var query = 'CREATE TABLE cache_store(id INTEGER PRIMARY KEY AUTOINCREMENT, key TEXT NOT NULL, value TEXT NOT NULL)';
utilDebug('websql.init query '+query);
executeSql(tx, query, [], function(tx) {
var query = 'CREATE UNIQUE INDEX cache_store__key__udx ON cache_store (key)';
utilDebug('websql.init query '+query);
executeSql(tx, query, [], nullDataHandler(cb), cb);
}, cb);
});
};
return {
cacheOpen: function(cb) {
if (cacheDB !== null) {
return cb(null); // open once.
}
try {
cacheDB = rootWindow.openDatabase(opt.name, opt.version, opt.name, opt.size);
} catch(e) {
return cb(e);
}
cacheDB.transaction(function(tx) {
var query = 'SELECT value FROM cache_store WHERE key = \"test-for-table\"';
utilDebug('websql.cacheOpen query '+query);
executeSql(tx, query, [], function() {
cb(null);
}, function() {
cacheDBInit(cb);
});
});
},
cacheGetValue: function(key, cb) {
cacheDB.transaction(function(tx) {
var query = 'SELECT value FROM cache_store WHERE key = ?';
utilDebug('websql.cacheGetValue query '+query);
executeSql(tx, query,[key], function(tx, res) {
if (res.rows.length === 0) {
cb(null, null);
} else {
var value = res.rows.item(0).value;
cb(null, JSON.parse(value));
}
}, cb);
});
},
cacheSetValue: function(key, value, cb) {
cacheDB.transaction(function(tx) {
var query = 'SELECT value FROM cache_store WHERE key = ?';
utilDebug('websql.cacheSetValue query '+query);
executeSql(tx, query,[key], function(tx, res) {
if (res.rows.length === 0) {
var queryInsert = 'INSERT INTO cache_store (key,value) VALUES (?,?)';
utilDebug('websql.cacheSetValue query '+queryInsert);
executeSql(tx, queryInsert, [key,JSON.stringify(value)], nullDataHandler(cb), cb);
} else {
var queryUpdate = 'UPDATE cache_store SET value = ? WHERE key = ?';
utilDebug('websql.cacheSetValue query '+queryUpdate);
executeSql(tx, queryUpdate, [JSON.stringify(value),key], nullDataHandler(cb), cb);
}
}, cb);
});
},
cacheDeleteValue: function(key, cb) {
cacheDB.transaction(function(tx) {
var query = 'DELETE FROM cache_store WHERE key = ?';
utilDebug('websql.cacheDeleteValue query '+query);
executeSql(tx, query, [key], function () {
setTimeout(nullDataHandler(cb)); // return next tick so transaction is flushed before location.reload
}, cb);
});
}
};
},
}
};
/**
* Prints the debug message with prefix to the options.debug.handler if options.debug.enable is true.
* @param {String} message The message to log.
* @private
*/
var utilDebug = function (message) {
if (options.debug.enable !== true) {
return;
}
options.debug.handler(options.debug.prefix+message);
};
/**
* The default error handler which renders the error in the browser.
* @param {String|Error} err The error message.
* @private
*/
var utilErrorHandler = function(err) {
if (!(err instanceof Error)) {
err = new Error(err);
}
utilDebug('utilErrorHandler error '+err.name+' '+err.message);
var rootTag = document.createElement('div');
rootTag.setAttribute('class','ffError');
document.getElementsByTagName('body')[0].appendChild(rootTag);
var cssTag = document.createElement('style');
cssTag.type = 'text/css';
cssTag.innerHTML = options.error.style;
rootTag.appendChild(cssTag);
var titleTag = document.createElement('h1');
titleTag.appendChild(document.createTextNode(options.error.title+err.name));
rootTag.appendChild(titleTag);
var questionTag = document.createElement('p');
questionTag.appendChild(document.createTextNode(err.message));
rootTag.appendChild(questionTag);
try {
var stack = err.stack || '';
stack = stack.split('\n').map(function (line) { return line.trim(); });
var stackText = stack.splice(stack[0] == 'Error' ? 2 : 1);
var traceTag = document.createElement('div');
traceTag.appendChild(document.createTextNode(stackText));
rootTag.appendChild(traceTag);
} catch (stackError) {
utilDebug('No stack: '+stackError);
}
};
/**
* Fetches an url resource with a XMLHttpRequest.
* @param {String} url The url to fetch.
* @param {function} cb The callback for error and request if ready.
* @private
*/
var utilHttpFetch = function (url, cb) {
var startTime = new Date().getTime();
var httpRequest = new XMLHttpRequest();
httpRequest.onreadystatechange = function() {
if (httpRequest.readyState === 4 && httpRequest.status === 200) {
utilDebug('utilHttpFetch url \"'+url+'\" done in '+(new Date().getTime()-startTime)+' ms.');
cb(null, httpRequest);
} else if (httpRequest.readyState === 4) {
cb('Wrong status '+httpRequest.status);
}
};
httpRequest.open('GET', url, true);
httpRequest.timeout = options.server.timeout; // ieX: after open()
httpRequest.ontimeout = function() {
cb('timeout after '+options.server.timeout+' of url '+url);
};
httpRequest.send();
};
var cacheGetService = function (type) {
if (options.cache[type]) {
return options.cache[type];
}
return null;
};
var cacheHasService = function (type) {
return cacheGetService(type) !== null;
};
var cacheCheckType = function (type, cb, action) {
cacheHasService(type)?action():cb(new Error('No caching for '+type));
};
var cacheGetValue = function(type, key , cb) {
cacheCheckType(type, cb, function() {
var cacheKey = type+'_'+key;
utilDebug('cacheGetValue key '+cacheKey);
cacheGetService(type).cacheGetValue(cacheKey,cb);
});
};
var cacheSetValue = function(type, key, value, cb) {
cacheCheckType(type, cb, function() {
var cacheKey = type+'_'+key;
utilDebug('cacheSetValue key '+cacheKey);
cacheGetService(type).cacheSetValue(cacheKey,value,cb);
});
};
var cacheDeleteValue = function(type, key, cb) {
cacheCheckType(type, cb, function() {
var cacheKey = type+'_'+key;
utilDebug('cacheDeleteValue key '+cacheKey);
cacheGetService(type).cacheDeleteValue(cacheKey,cb);
});
};
// var cleanupCache = function (resources, cb) {
// utilDebug('cleanupCache TODO cacheList '+0+' resources '+resources.length);
// // TODO: impl for removes in resource lists
// cb(null);
// };
var injectResourceData = function(resource, data, cb) {
utilDebug('injectResourceData resource '+JSON.stringify(resource)+' data '+data.length);
var tag = null;
if (resource.type === 'css' || resource.type === 'cssData') {
tag = document.createElement('style');
tag.type = 'text/css';
}
if (resource.type === 'js') {
tag = document.createElement('script');
tag.type = 'text/javascript';
}
tag.appendChild(document.createTextNode(data));
document.getElementsByTagName('head')[0].appendChild(tag);
//var ref = document.getElementsByTagName('script')[0];
//ref.parentNode.insertBefore(tag, ref); // note in reverse order
cb(null);
};
var storeResourceKey = function (resource, cb) {
// Add all cache keys in central list so we can clear the cache item if resources are removed.
var cacheKey = resource.type+'_keys';
cacheGetValue('meta',cacheKey,function (err,value) { // TODO: move to resource.type
if (err !== null) {
cb(err);
return;
}
if (value === null) {
value = [];
}
value.push(resource.hash);
cacheSetValue('meta',cacheKey,value, cb);
});
};
var storeResource = function (resource, httpRequest, cb) {
utilDebug('storeResource url '+resource.url+' hash '+resource.hash);
var item = {
resource: resource,
data: httpRequest.responseText
};
cacheSetValue(resource.type, resource.hash, item , function(err) {
if (err !== null) {
cb(err);
} else {
storeResourceKey(resource,cb);
}
});
};
var loadResource = function (resource, cb) {
var resourceUrl = resource.url;
if (resourceUrl.indexOf('http') === -1) {
resourceUrl = options.server.url + resourceUrl;
}
utilDebug('loadResource '+JSON.stringify(resource));
if (cacheHasService(resource.type)) {
cacheGetValue(resource.type,resource.hash,function(err, value) {
if (err !== null) { return cb(err); }
if (value === null) {
utilDebug('loadResource cache miss'); // + hash mismatch as its the key
} else if (value.resource === undefined) {
utilDebug('loadResource cache wrong obj');
} else {
utilDebug('loadResource cache hit');
injectResourceData(value.resource,value.data,cb);
return;
}
utilHttpFetch(resourceUrl, function(err, httpRequest) {
if (err !== null) { return cb(err); }
storeResource(resource, httpRequest, function (err) {
if (err !== null) { return cb(err); }
injectResourceData(resource,httpRequest.responseText,cb);
});
});
});
} else {
// note: was links but now download + inject so order stays sequenced
utilHttpFetch(resourceUrl, function(err, httpRequest) {
if (err !== null) { return cb(err); }
injectResourceData(resource,httpRequest.responseText,cb);
});
}
};
var loadResources = function(resources, cb) {
var startTime = new Date().getTime();
utilDebug('loadResources');
var resourceStack = resources;
var resourceLoader = function(err) {
if (err !== null) { return cb(err); }
resourceStack = resourceStack.slice(1);
if (resourceStack.length === 0) {
utilDebug('loadResources done in '+(new Date().getTime()-startTime)+' ms.');
cb(null);
} else {
loadResource(resourceStack[0],resourceLoader);
}
};
loadResource(resourceStack[0],resourceLoader);
};
var injectResource = function(resource, cb) {
cacheGetValue(resource.type,resource.hash,function(err,item) {
injectResourceData(resource,item.data,cb);
});
}
var injectResources = function(resources, cb) {
var startTime = new Date().getTime();
utilDebug('injectResources');
var resourceStack = resources;
var resourceLoader = function(err) {
if (err !== null) { return cb(err); }
resourceStack = resourceStack.slice(1);
if (resourceStack.length === 0) {
utilDebug('injectResources done in '+(new Date().getTime()-startTime)+' ms.');
cb(null);
} else {
injectResource(resourceStack[0],resourceLoader);
}
};
injectResource(resourceStack[0],resourceLoader);
};
/**
* Starts loader by downloading resource list or using cache version to
* load/refresh/inject the resources.
*
* @param {function} cb Callback gets called when loader is done.
* @private
*/
var startLoader = function (cb) {
if (options.server.url === null) {
cb(new Error('No url'));
return;
}
if (options.server.assets === null) {
cb(new Error('No assets'));
return;
}
rootWindow[options.server.flag] = options.server.url;
var resourcesUrl = options.server.url + options.server.assets;
utilDebug('startLoader assets \"'+resourcesUrl+'\"');
utilHttpFetch(resourcesUrl, function(err, httpRequest) {
if (err !== null) {
utilDebug('startLoader fetch error '+err);
if (cacheHasService('meta')) {
cacheGetValue('meta','server_resources',function(err, value) {
if (err !== null) {
cb(err);
} else if (value === null) {
cb(new Error('Have no cache of server resouces from '+options.server.url));
} else {
injectResources(value, cb);
}
});
} else {
cb(new Error('Could not fetch server resouces from '+options.server.url));
}
return;
}
var resources = [];
JSON.parse(httpRequest.responseText).data.resources.forEach(function (r) {
resources.push(r);
});
utilDebug('startLoader resources '+resources.length);
if (cacheHasService('meta')) {
cacheSetValue('meta','server_resources',resources, function (err) {
if (err !== null) { return cb(err); }
loadResources(resources, cb);
});
} else {
loadResources(resources, cb);
}
});
};
/**
* Validates the user input url by downloading it.
*
* @param {function} cb Callback gets called when loader is done.
* @param {Element} deleteTag The dom element to remove from the body tag after success.
* @private
*/
var askUrlValidate = function (cb,deleteTag) {
var inputTag = document.getElementById('serverInput');
var inputErrorTag = document.getElementById('serverInputError');
while (inputErrorTag.firstChild) {
inputErrorTag.removeChild(inputErrorTag.firstChild); // clear error
}
options.server.url = '';
if (inputTag.value.indexOf('http') === -1) {
options.server.url += options.server.question.transport;
}
options.server.url += inputTag.value;
// TODO: auto rm ending /
var resourcesUrl = options.server.url + options.server.assets;
utilDebug('askUrlStart check assets '+resourcesUrl);
// TODO: add+check headers
utilHttpFetch(resourcesUrl,function(err, httpRequest) {
if (err !== null) {
inputErrorTag.appendChild(document.createTextNode('Error could not get data.'));
return;
}
if (httpRequest.responseText.length === 0) {
inputErrorTag.appendChild(document.createTextNode('Error Got empty data.'));
return;
}
document.getElementsByTagName('body')[0].removeChild(deleteTag);
if (cacheHasService('meta')) {
cacheSetValue('meta','server_url',options.server.url, function(err) {
if (err !== null) { return cb(err); }
startLoader(cb);
});
} else {
startLoader(cb);
}
});
};
/**
* Creates the ask url ui.
*
* @param {function} cb Callback gets called when loader is done.
* @private
*/
var askUrl = function (cb) {
utilDebug('askUrl create ui');
var rootTag = document.createElement('div');
rootTag.setAttribute('class','ffAskUrl');
var cssTag = document.createElement('style');
cssTag.type = 'text/css';
cssTag.innerHTML = options.server.question.style;
rootTag.appendChild(cssTag);
var titleTag = document.createElement('h1');
titleTag.appendChild(document.createTextNode(options.server.question.title));
rootTag.appendChild(titleTag);
var questionTag = document.createElement('p');
questionTag.appendChild(document.createTextNode(options.server.question.text));
rootTag.appendChild(questionTag);
var formTag = document.createElement('div');
rootTag.appendChild(formTag);
var transportTag = document.createElement('label');
rootTag.setAttribute('for','serverInput');
transportTag.appendChild(document.createTextNode(options.server.question.transport));
formTag.appendChild(transportTag);
var inputTag = document.createElement('input');
inputTag.type = 'text';
inputTag.id = 'serverInput';
inputTag.setAttribute('autofocus','');
inputTag.setAttribute('onkeydown','if (event.keyCode == 13) {document.getElementById(\'serverSubmit\').click()}');
formTag.appendChild(inputTag);
var submitLineTag = document.createElement('br');
formTag.appendChild(submitLineTag);
var submitTag = document.createElement('input');
submitTag.id = 'serverSubmit';
submitTag.type = 'submit';
submitTag.value = 'Start';
submitTag.onclick = function() {askUrlValidate(cb,rootTag);};
formTag.appendChild(submitTag);
var serverErrorTag = document.createElement('div');
serverErrorTag.id = 'serverInputError';
serverErrorTag.setAttribute('class','ffAskUrlError');
formTag.appendChild(serverErrorTag);
document.getElementsByTagName('body')[0].appendChild(rootTag);
};
var startCacheType = function (type,cb) {
if (options.cache[type] !== null && options.cache[type] === false) {
utilDebug('startCacheType '+type+' disabled');
options.cache[type] = null;
return cb(null);
}
if (options.cache[type] === null) {
if (factory.detect.cordovaDevice() && factory.detect.sqlitePlugin()) {
utilDebug('startCacheType auto sqlitePlugin for '+type);
options.cache[type] = factory.cache.websql({openDatabase: rootWindow.sqlitePlugin});
} else if (factory.detect.openDatabase()) {
utilDebug('startCacheType auto openDatabase for '+type);
options.cache[type] = factory.cache.websql();
} else if (factory.detect.localStorage()) {
utilDebug('startCacheType auto localStorage for '+type);
options.cache[type] = factory.cache.localStorage();
} else {
utilDebug('startCacheType '+type+' none');
}
}
if (options.cache[type] !== null && typeof options.cache[type].cacheOpen === 'function') {
options.cache[type].cacheOpen(cb);
} else {
cb(null);
}
};
var startCache = function (cb) {
startCacheType('meta', function(err) {
if (err !== null) { return cb(err); }
startCacheType('js', function(err) {
if (err !== null) { return cb(err); }
startCacheType('css', function(err) {
if (err !== null) { return cb(err); }
startCacheType('cssData', cb);
});
});
});
};
/**
* Starts the loader.
*
* @param {function} cb Callback gets called when loader is done.
*/
var start = function (cbArgu) {
var startTime = new Date().getTime();
var cb = function(err) {
if (err !== null) {
options.error.handler(err);
} else {
utilDebug('start done in '+(new Date().getTime()-startTime)+' ms.');
bootAngular(function(err) {
if (err !== null) { return options.error.handler(err); }
if (typeof cbArgu === 'function') {
cbArgu();
}
});
}
};
if (options.debug.enable === true) {
var optionsKeys = Object.keys(options);
for (var keyId in optionsKeys) {
var key = optionsKeys[keyId];
utilDebug('start config '+key+' '+JSON.stringify(options[key]));
}
}
bootCordova(function(err) {
if (err !== null) { return cb(err); }
startCache(function(err) {
if (err !== null) { return cb(err); }
if (options.server.url !== null) {
startLoader(cb);
return;
}
if (cacheHasService('meta')) {
cacheGetValue('meta','server_url',function(err, value) {
if (err !== null) {
cb(err);
} else if (value === undefined || value === null || value === '') {
askUrl(cb);
} else {
options.server.url = value;
startLoader(cb);
}
});
} else {
askUrl(cb);
}
});
});
};
var clearServerUrl = function(cb) {
if (cb === undefined) {
cb = function() {};
}
if (cacheHasService('meta')) {
cacheDeleteValue('meta','server_url',cb);
} else {
cb(null);
}
};
var bootAngular = function(cb) {
if (options.boot.angular.enable !== true) {
utilDebug('bootAngular disabled by options');
return cb(null);
}
if (options.boot.angular.modules.length === 0) {
utilDebug('bootAngular disabled by no modules');
return cb(null);
}
utilDebug('bootAngular start '+options.boot.angular.modules);
angular.bootstrap(document, options.boot.angular.modules);
cb(null);
};
/**
* Helper for cordova applications which want to use the sqllite plugin as cache.
* Note: On none cordova page it will callback directly.
* Note: On none cordova device page is will timeout.
*
* @param {function} cb Callback gets called device is ready to use.
* @private
*/
var bootCordova = function(cb) {
if (options.boot.cordova.enable !== true) {
utilDebug('bootCordova disabled by options');
return cb(null);
}
if (factory.detect.cordova() !== true) {
utilDebug('bootCordova disabled by detect');
return cb(null);
}
var startTime = new Date().getTime();
var bootOnce = function() {
var callback = cb;
cb = null;
utilDebug('bootCordova done in '+(new Date().getTime()-startTime)+' ms.');
callback();
};
if (options.boot.cordova.timeout > 0) {
utilDebug('bootCordova timeout '+options.boot.cordova.timeout);
setTimeout ( function () {
utilDebug('bootCordova timeout');
bootOnce();
}, options.boot.cordova.timeout);
}
document.addEventListener('deviceready', function () {
rootWindow[options.boot.cordova.flag] = true;
utilDebug('bootCordova '+options.boot.cordova.flag);
bootOnce();
}, false);
};
// Auto fill handlers and return public object.
options.debug.handler = function(msg) {console.log(msg);};
options.error.handler = utilErrorHandler;
return {
options: options,
factory: factory,
start: start,
clearServerUrl: clearServerUrl
};
});