'use strict'; /* * 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. */ // TODO: // conv to req queue // split js/css so css can be done later // set tag.media = 'only you'; // add media in resouces // add userAgant match in resouces // NOTE: add ordered inject, for now JS in non-cache mode needs to be aggragated server side // add browser supported var FFSpaLoader = (function () { var options = {}; var utilDebug = function (message) { if (options.debug !== true) { return; } console.log('FFSpaLoader.'+message); }; var utilHttpFetch = function (url, cb) { var startTime = new Date().getTime(); var httpRequest = new XMLHttpRequest(); httpRequest.onreadystatechange = function() { if (httpRequest.readyState == 4) { utilDebug('utilHttpFetch url: '+url+' done in '+(new Date().getTime()-startTime)+' ms.'); cb(httpRequest); } } httpRequest.open('GET', url, true); httpRequest.send(); }; var cacheHasItem = function (resource) { if (options.cacheGetFn === undefined || options.cacheSetFn === undefined || options.cacheDelFn === undefined) { return false; } var result = options.cacheGetFn(options.cachePrefix + resource.hash) !== null; utilDebug('cacheHasItem resource: '+resource.url+' result: '+result); return result; } var cacheGetItem = function (resource) { if (options.cacheGetFn === undefined) { throw new Error('no caching'); } utilDebug('cacheGetItem resource: '+resource.url); return JSON.parse(options.cacheGetFn(options.cachePrefix + resource.hash)); } var cachePutRequest = function (resource, httpRequest) { if (options.cacheSetFn === undefined) { throw new Error('no caching'); } var cacheKey = options.cachePrefix + resource.hash; var data = httpRequest.responseText; utilDebug('cachePutData url: '+resource.url+' key: '+cacheKey); options.cacheSetFn(cacheKey, JSON.stringify({ resource: resource, data: data, dataDate: new Date().getTime() })); // Add all cache keys in central list so we can clear the cache item if resources are removed. var cacheListKey = options.cachePrefix + 'cache-list'; var cacheListStr = options.cacheGetFn(cacheListKey); if (cacheListStr === null) { cacheListStr = '[]'; } var cacheList = JSON.parse(cacheListStr); cacheList.push(cacheKey); options.cacheSetFn(cacheListKey, JSON.stringify(cacheList)); } var cacheCleanup = function (resources) { if (options.cacheGetFn === undefined || options.cacheSetFn === undefined || options.cacheDelFn === undefined) { return; } var cacheListKey = options.cachePrefix + 'cache-list'; var cacheListStr = options.cacheGetFn(cacheListKey); if (cacheListStr === null) { return; } var cacheList = JSON.parse(cacheListStr); utilDebug('cacheCleanup TODO cacheList: '+cacheList.length+' resources: '+resources.length); // TODO: impl for removes in resource lists } var createResource = function (resource, cb) { utilDebug('createResource url: '+JSON.stringify(resource)); if (cacheHasItem(resource)) { if (cacheGetItem(resource).resource.hash === resource.hash) { cb(); return; } utilDebug('createResource hash mismatch request fetch: '+resource.url); } else { utilDebug('createResource cache miss request fetch: '+resource.url); } utilHttpFetch(resource.url, function(httpRequest) { if (httpRequest.status == 200) { cachePutRequest(resource, httpRequest); } else { console.warn('error loading '+resource.url); } cb(); }); } var createResources = function(resources, cb) { var startTime = new Date().getTime(); utilDebug('createResources'); var resourceStack = resources; var resourceLoader = function() { resourceStack = resourceStack.slice(1); if (resourceStack.length === 0) { utilDebug('createResources done in '+(new Date().getTime()-startTime)+' ms.'); cb(); } else { createResource(resourceStack[0],resourceLoader); } }; createResource(resourceStack[0],resourceLoader); } var injectResources = function(resources, cb) { var startTime = new Date().getTime(); utilDebug('injectResources'); resources.forEach(function (resource) { var item = cacheGetItem(resource); var tag = null; if (resource.type === 'css') { tag = document.createElement('style'); tag.type = 'text/css'; } else { tag = document.createElement('script'); tag.type = 'text/javascript'; } utilDebug('injectResources resource: '+JSON.stringify(resource)); tag.appendChild(document.createTextNode(item.data)); //document.getElementsByTagName('head')[0].appendChild(tag); var ref = document.getElementsByTagName('script')[0]; ref.parentNode.insertBefore(tag, ref); // note in reverse order }); utilDebug('injectResources done in '+(new Date().getTime()-startTime)+' ms.'); cb(); } var injectLinkResources = function(resources, cb) { var startTime = new Date().getTime(); utilDebug('injectLinkResources'); resources.forEach(function (resource) { utilDebug('injectLinkResources resource: '+JSON.stringify(resource)); var tag = null; if (resource.type === 'css') { tag = document.createElement('link'); tag.type = 'text/css'; tag.rel = 'stylesheet'; tag.href = resource.url; } if (resource.type === 'js') { tag = document.createElement('script'); tag.type = 'text/javascript'; tag.src = resource.url; } if (tag !== null) { var startTime2 = new Date().getTime(); document.getElementsByTagName('head')[0].appendChild(tag); utilDebug('inject time in '+(new Date().getTime()-startTime2)+' ms.'); //var ref = document.getElementsByTagName('script')[0]; //ref.parentNode.insertBefore(tag, ref); // TODO reverser order } else { utilDebug('unknow resource type: '+resource.type); // TODO add err } }); utilDebug('injectLinkResources done in '+(new Date().getTime()-startTime)+' ms.'); cb(); } var startDefaults = function () { if (options.url === undefined) { throw new Error('No url defined'); } if (options.debug === undefined) { options.debug = false; } if (options.cachePrefix === undefined) { options.cachePrefix = 'sync-'; } } var startDone = function (startTime) { utilDebug('start done in '+(new Date().getTime()-startTime)+' ms.'); if (options.resultFn) { options.resultFn(); } } var start = function (opt) { if (opt === undefined) { throw new Error('need options'); } var startTime = new Date().getTime(); options = opt; startDefaults(); var injectAsLink = options.cacheGetFn === undefined || options.cacheSetFn === undefined || options.cacheDelFn === undefined; utilDebug('start options: '+JSON.stringify(options)+' injectAsLink: '+injectAsLink); var resources = []; utilHttpFetch(options.url, function(httpRequest) { if (httpRequest.status == 200) { JSON.parse(httpRequest.responseText).data.resources.forEach(function (r) { resources.push(r); utilDebug('build resources add: '+JSON.stringify(r)); }); if (injectAsLink) { injectLinkResources(resources, function () { startDone(startTime); }); } else { createResources(resources, function () { injectResources(resources, function () { cacheCleanup(resources); startDone(startTime); }); }); } } else { console.log('app err'); // TODO: fix async load error code path } }) }; var getOrAskUrl = function (opt) { }; // TODO: getOrAskUrl: getOrAskUrl, return { start: start }; })(); var autoStartFFSpaLoader = function () { var scripts = document.getElementsByTagName('SCRIPT'); if (scripts && scripts.length>0) { for (var i in scripts) { if (scripts[i].src && scripts[i].src.match(/.*ffSpaLoaderUrl.*/)) { var query = scripts[i].src; var queryParameters = {}; query.split('?').forEach(function(part) { var item = part.split("="); queryParameters[item[0]] = decodeURIComponent(item[1]); }); // TODO: remove debug here FFSpaLoader.start({ debug: true, injectAsLink: true, url: queryParameters.ffSpaLoaderUrl }); break; } } } }; autoStartFFSpaLoader(); // TODO: make private