"use strict"; const Mimicry = (function privateMimicryClosure () { class Mimicry { /** * Creates an instance of the Mimicry. * * @param {Object} [config] - configuration of the Mimicry. * @param {Number} [config.maxParallelLoadings] - amount of files that are loaded in parallel. * @param {String} [config.baseUrl] - URL prefix that will be added to every. * @param {String} [config.seed] - request optional component allowing to drop user cached files. */ constructor (config) { instances.push(this); this._queue = null; //deferred Queue this._loadedFiles = new Set(); this._config = Object.create(null); this._loadingFiles = new Map(); this._plannedFiles = Object.create(null); this._modules = new Map(); this._typePaths = new Map(); this._loadedAssets = new Map(); this._libs = new Map(); this._errors = new Map(); this._moduleErrors = []; this._errorHandlers = []; this._progressHandlers = []; Object.assign(this._config, defaultOptions, config); this._baseUrl = this._config.baseUrl; if (this._baseUrl && !this._baseUrl.endsWith("/")) this._baseUrl += "/"; this._globalErrorHandler = onGlobalError.bind(this); window.addEventListener("error", this._globalErrorHandler); if (ready) createDeferredParts.call(this); else this._deferred = []; } /** * Finalizes Mimicry, clears as much memory as it can, forgets all the handlers and contexts * */ destructor () { window.off("error", this._globalErrorHandler); const index = instances.indexOf(this); instances.splice(index, 1); delete this._queue; delete this._loadedFiles; delete this._config; delete this._loadingFiles; delete this._plannedFiles; delete this._modules; delete this._typePaths; delete this._loadedAssets; delete this._libs; delete this._errors; delete this._moduleErrors; delete this._errorHandlers; delete this._progressHandlers; delete this._globalErrorHandler; delete this._deferred; delete this._baseUrl; this.destroyed = true; } /** * Loads a file, calls the callback if it has loaded successfully, * calls error callback if there was any sort of error * * @param {String} path - path to the module * @param {Function} [callback=contentType] - a callback that is called after module is loaded * @param {Function} [error=contentType] - a callback that is called if there was an error * @param {Mimicry.Type} [contentType=Mimicry.Type.js] - type of the content to load */ load (path, callback, error, contentType) { if (!ready) { this._deferred.push(["load", path, callback, error, contentType]); return; } if (!(error instanceof Function) && contentType === undefined) { contentType = error; error = undefined; } if (!(callback instanceof Function) && contentType === undefined) { contentType = callback; callback = undefined; } if (!contentType) contentType = Type.js; scheduleLoad.call(this, Dependency.refinePath(path, contentType, this._baseUrl, this._typePaths, this._libs), callback, error, contentType, path); } /** * Declares a module. * The mainstream way to use Mimicry is to declare modules. * It's best If you declare one module per file. * * The best way to export anything from the module is to return it from the callback * * @param {Array} dependencies - list of module names that this module depends on * @param {Function} [callback] - module body that will be executed as soon all dependencies are resolved * @param {HTMLScriptElement} [script] - script element launching this module (you don't need to pass it, it's automatic) */ module (dependencies, callback, script) { if (!ready) { this._deferred.push(["module", dependencies, callback, document.currentScript]); return; } const href = getCurrentHref(script); if (!href.length) throw new Error("Couldn't understand module path"); const moduleName = Dependency.nameFromHref(href, this._baseUrl, this._typePaths, this._libs); if (this._modules.has(moduleName)) throw new Error("An attempt to declare more than one module per file (" + moduleName + ")"); const {deps, body} = Dependency.refineModuleArguments(dependencies, callback); const module = new Module(deps, body); this._modules.set(moduleName, module); for (let i = 0; i < deps.length; ++i) { const element = deps[i]; const contentType = element.contentType; const elName = element.name = fromRelative(element.name, moduleName); this.load(elName, loadedFileForModule.bind(this, module, elName), contentType); } if (module.allDependenciesResolved) module.execute(this, window); } /** * Synchronously returns the module value if it was * already loaded and executed, returns undefined otherwise * */ getModule(/*String*/moduleName) { const module = this._modules.get(moduleName); if (module && module.executed) return module.value; } /** * Synchronously returns the asses if it was already loaded, returns undefined otherwise * */ getAsset(/*String*/assetName) { const asset = this._loadedAssets.get(assetName); if (asset) return asset; } /** * If you wish to keep all files of some types in a designated folder of your project * you can declare it using this function. * * Say, you want to have all JSON files in "json" folder, and all CSS files in "styles/css" folder. * After calling "mimicry.setTypePath(Mimicry.Type.json, 'json')" and * "mimicry.setTypePath(Mimicry.Type.css, 'styles/css')" you can access files from those folders * in a following manner * * "mimicry.load('json/config/main', () => {}, Mimicry.Type.json)" changes to * "mimicry.load('config/main', () => {}, Mimicry.Type.json)", and * * "mimicry.load('styles/css/basic', () => {}, Mimicry.Type.css)" to * "mimicry.load('basic', () => {}, Mimicry.Type.css)" * */ setTypePath (/*Type*/type, /*String*/path) { if (path && !path.endsWith("/")) path += "/"; this._typePaths.set(type, path); } /** * You might want to include other projects in your project. I will call those imported subprojects libraries * In mimicry there is a way to hide actual path behind library name. * * Say you included a library called "flame", and placed in "libs/external/flame". * After calling "mimicry.setLibPath('flame', 'libs/external/flame')" you can access files from those libraries * in a following manner * * "mimicry.load('libs/external/flame/index', () => {})" changes to * "mimicry.load('@flame/index', () => {})" * */ setLibPath (/*String*/libraryName, /*String*/libraryPath) { const path = this._libs.get(libraryName); if (path && path !== libraryPath) throw new Error("Mimicry error: library " + libraryName + " already had a path " + path + ", assigning new path " + libraryPath + " would probably break everything"); this._libs.set(libraryName, libraryPath); } /** * Adds a handler for you to handle errors that this instance of mimicry * faces loading and evaluating files. * * These handlers get called after (if there was any) error callbacks passed to load operations. * * @param {Function} handler - a handler that would be called in case of error * * Handler is called with a single argument of error, which can one of the following types: * - Mimicry.FileError - if there was an error on file evaluation * - Mimicry.LoadError - if mimicry could not load a file * */ addErrorHandler (handler) { this._errorHandlers.push(handler); } /** * Method provides you information about the progress of your loads. * Sometimes when you load lots of files in bulk you wish to show a progress indicator. * * @param {Function} handler - a handler which is called when the progress changes * Passed handler is called with following arguments: * {Number} loaded - how many files mimicry have loaded so far * {Number} pending - how many files are currently loading or in a queue * {Number} total - how many files does mimicry even know about * */ addProgressHandler (handler) { this._progressHandlers.push(handler); } /** * If you use Mimicry in a script file included right after mimicry * it might not be able to work yet: it doesn't yet have some classes for normal work. * In this "unready" state you can not * - access Mimicry.FileError nad Mimicry.LoadError classes * - access Mimicry.Type enum. * * You can, however, perform default (meaning Mimicry.type.js) loads, declare modules, * set up paths, and subscribe to handlers. * * @returns {Boolean} - if the instance is ready to be used * */ get ready () {return ready;} /** * @returns {Number} - amount of files that are currently loading or in a queue * */ get pendingFilesAmount () {return this._loadingFiles.size + this._queue.length;} /** * @returns {Number} - amount of files mimicry have loaded until this moment * */ get loadedFilesAmount () {return this._loadedFiles.size;} /** * @returns {Object} - default mimicry options object * */ static get defaultOptions () {return defaultOptions;} /** * If you use Mimicry in a script file included right after mimicry * it might not be able to work yet: it doesn't yet have some classes for normal work. * In this "unready" state you can not * - access Mimicry.FileError nad Mimicry.LoadError classes * - access Mimicry.Type enum. * * You can, however, perform default (meaning Mimicry.type.js) loads, declare modules, * set up paths, and subscribe to handlers. * * This state is on the static level: once all necessary files are loaded all mimicry instances are * ready and don't need to be waited on * * @returns {Boolean} - if the Mimicry system is ready to be used * */ static get ready () {return ready;} /** * Normally mimicry loads .js files, but you can also load other stuff like json, or css, or binary * I expect you to pass values from this enum to specify what exactly you are loading if it is not a * JavaScript file * * @returns {Mimicry.Type} - type of content * */ static get Type () {return Type;} static get FileError() {return FileError;} static get LoadError() {return LoadError;} } const defaultOptions = { maxParallelLoadings: 10, baseUrl: "", seed: "" }; Object.freeze(defaultOptions) function createDeferredParts () { this._queue = new Queue(); } function launchDeferredRequests () { if (this._deferred instanceof Array) { while (this._deferred.length) { const args = this._deferred.shift(); const methodName = args.shift(); this[methodName].apply(this, args); } delete this._deferred; } } function callCallback(/*Function*/callback, /*String*/originalName) { if (callback instanceof Function) { const module = this._modules.get(originalName); if (module) { module.addCallback(callback, window); } else { const asset = this._loadedAssets.get(originalName); if (asset) callback(asset); else callback(window); } } } /** * @param {String} path * @param {Function} callback * @param {Function} error * @param {Type} contentType * @param {String} originalName * */ function scheduleLoad (path, callback, error, contentType, originalName) { if (this._loadedFiles.has(path)) return callCallback.call(this, callback, originalName); let loadingFile = this._loadingFiles.get(path); if (loadingFile === undefined) loadingFile = this._plannedFiles[path]; if (loadingFile) { Dependency.checkContentType(loadingFile.contentType, contentType, path); loadingFile.addSuccessHandler(callback); loadingFile.addErrorHandler(error); return; } loadingFile = new LoadingFile(contentType, originalName, callback, error); if (this._loadingFiles.size < this._config.maxParallelLoadings) { executeLoad.call(this, path, loadingFile); } else { this._queue.push(path); this._plannedFiles[path] = loadingFile; } notifyProgressSubscribers.call(this); } function executeLoad (/*String*/path, /*LoadingFile*/loadingFile) { this._loadingFiles.set(path, loadingFile); const boundLoad = onLoaded.bind(this, path); const boundError = onError.bind(this, path); switch (loadingFile.contentType) { case Type.js: getScriptWithTag(path, boundLoad, boundError, this._config.seed); break; case Type.css: getStylesheetWithTag(path, boundLoad, boundError, this._config.seed); break; case Type.json: getByXHR(path, boundLoad, boundError, "json", this._config.seed); break; case Type.binary: getByXHR(path, boundLoad, boundError, "arraybuffer", this._config.seed); break; case Type.text: getByXHR(path, boundLoad, boundError, "text", this._config.seed); break; } } function notifyProgressSubscribers() { const pending = this.pendingFilesAmount; const loaded = this.loadedFilesAmount; for (let i = 0; i < this._progressHandlers.length; ++i) { const handler = this._progressHandlers[i]; handler(loaded, pending, loaded + pending); } } function onLoaded(/*String*/path, /*HTMLScriptElement*/event) { if (this._moduleErrors.length) return onModuleError.call(this, path, event); if (this._errors.has(path)) return onError.call(this, path, event); const loading = this._loadingFiles.get(path); this._loadedFiles.add(path); switch (loading.contentType) { case Type.json: case Type.binary: case Type.text: this._loadedAssets.set(loading.originalName, event.target.response); break; } loading.callSuccessHandlers(callCallback, this); cleanUp.call(this, path, event); checkNextElement.call(this); notifyProgressSubscribers.call(this); } function onError (/*String*/path, /*HTMLScriptElement*/element) { const loading = this._loadingFiles.get(path); let error = this._errors.get(path); if (!(error instanceof BasicError)) error = new LoadError(path, loading.originalName); loading.callErrorHandlers(error); for (let i = 0; i < this._errorHandlers.length; ++i) this._errorHandlers[i](error); cleanUp.call(this, path, element); checkNextElement.call(this); notifyProgressSubscribers.call(this); } function onModuleError (/*String*/path, /*HTMLScriptElement*/element) { for (let i = 0; i < this._moduleErrors.length; ++i) { const error = this._moduleErrors[i]; error.path = path; for (let i = 0; i < this._errorHandlers.length; ++i) this._errorHandlers[i](error) } this._moduleErrors = []; cleanUp.call(this, path, element); checkNextElement.call(this); notifyProgressSubscribers.call(this); } function cleanUp (/*String*/path, /*Event*/element) { const loading = this._loadingFiles.get(path); this._loadingFiles.delete(path); this._errors.delete(path); switch (loading.contentType) { case Type.js: cleanTag(element.target); break; case Type.css: cleanUpHandlers(element.target); break; case Type.json: case Type.binary: case Type.text: cleanUpHandlers(element); break } loading.destructor(); } function cleanTag (/*HTMLElement or Event*/tag) { tag.remove(); cleanUpHandlers(tag); } function cleanUpHandlers (/*HTMLElement*/tag) { delete tag.onload; delete tag.onerror; } function onGlobalError (event, url, line, col, errorObj) { let message; if (event instanceof ErrorEvent) { url = event.filename; line = event.lineno; col = event.colno; errorObj = event.error; message = event.message; } else { message = event; } if (url && url.indexOf(location.origin) === 0) url = url.slice(location.origin.length); if (!setFileError(url, message, line, col, errorObj)) { let href = getCurrentHref(); const index = url.lastIndexOf(href); if (href.length && index === url.length - href.length) setFileError(href, message, line, col, errorObj) } } function setFileError(name, message, line, col, errorObj) { let result = true; let loading = this._loadingFiles.get(name); if (loading instanceof LoadingFile) this._errors.set(name, new FileError(name, loading.originalName, message, line, col, errorObj)); else result = false; return result; } function checkNextElement () { while (!this._queue.empty && (this._loadingFiles.size < this._config.maxParallelLoadings)) { const path = this._queue.pop(); const loadingFile = this._plannedFiles[path]; delete this._plannedFiles[path]; executeLoad.call(this, path, loadingFile); } } /** * @param {String} path - path of the file you wish to load * @param {Function} [callback] - handler that would be called upon successful load * @param {Function} [error] - handler that would ba called upon error * @param {String} [seed] - a short seed to avoid unwanted caching * */ function getScriptWithTag (path, callback, error, seed) { const tag = document.createElement("script"); tag.setAttribute("async", ""); document.head.appendChild(tag); tag.onload = callback; tag.onerror = error; if (seed) path += "?seed=" + seed; tag.src = path; } /** * @param {String} path - path of the file you wish to load * @param {Function} [callback] - handler that would be called upon successful load * @param {Function} [error] - handler that would ba called upon error * @param {String} [seed] - a short seed to avoid unwanted caching * */ function getStylesheetWithTag (path, callback, error, seed) { const tag = document.createElement("link"); tag.setAttribute("type", "text/css"); tag.setAttribute("rel", "stylesheet"); document.head.appendChild(tag); tag.onload = callback; tag.onerror = error; if (seed) path += "?seed=" + seed; tag.href = path; } /** * @param {String} url * @param {Function} callback * @param {Function} error * @param {XMLHttpRequestResponseType} [responseType="arraybuffer"] * @param {String} [seed=""] * */ function getByXHR (url, callback, error, responseType="arraybuffer", seed="") { const xhr = new XMLHttpRequest(); if (seed) url += "?seed=" + seed; xhr.open("GET", url, true); xhr.responseType = responseType; xhr.onload = callback.bind(xhr); xhr.onerror = error.bind(xhr); xhr.send(); } function getCurrentHref(src) { let possible = document.currentScript; if (src instanceof HTMLScriptElement) possible = src; const script = possible || launcher; if (script instanceof HTMLScriptElement) return script.attributes.src.value; else return ""; } function loadedFileForModule (/*Module*/requester, /*String*/fileName) { const dep = this._modules.get(fileName); if (dep) dep.addCallback(requester.incrementDependency.bind(requester, this, window), window); else requester.incrementDependency(this, window); } const launcher = document.currentScript; const instances = []; let BasicError, LoadError, FileError; let LoadingFile; let Queue; let Dependency, Type; let Module; let ready = false; let errorsReady = false; let fileReady = false; let queueReady = false; let dependencyReady = false; let moduleReady = false; function checkDeferred () { if (!ready) { if (errorsReady && fileReady && queueReady && dependencyReady && moduleReady) { ready = true; for (let i = 0; i < instances.length; ++i) { const instance = instances[i]; createDeferredParts.call(instance); launchDeferredRequests.call(instance); } if (launcher instanceof HTMLScriptElement) { const main = launcher.dataset.main if (main) getScriptWithTag(main); } } } } let pathPrefix = directory(launcher.src); if (pathPrefix !== "") pathPrefix += "/"; function directory (path) { const hops = path.split("/"); hops.pop(); return hops.join("/"); } function fromRelative (path, reference) { let indexSame = path.indexOf("./"); let indexParent = path.indexOf("../"); if (indexSame === 0 || indexParent === 0) { reference = directory(reference); while (indexSame === 0 || indexParent === 0) { if (indexSame === 0) { path = path.slice(2); } else { reference = directory(reference); path = path.slice(3); } indexSame = path.indexOf("./"); indexParent = path.indexOf("../"); } let result = reference; if (result !== "") result += "/"; result += path return result } return path; } getScriptWithTag(pathPrefix + "errors.js", function (event) { cleanTag(event.target); BasicError = window.__MimicryErrors__.BasicError; LoadError = window.__MimicryErrors__.LoadError; FileError = window.__MimicryErrors__.FileError; delete window.__MimicryErrors__; errorsReady = true; checkDeferred(); }); getScriptWithTag(pathPrefix + "loadingFile.js", function (event) { cleanTag(event.target); LoadingFile = window.__MimicryLoadingFile__; delete window.__MimicryLoadingFile__; fileReady = true; checkDeferred(); }); getScriptWithTag(pathPrefix + "queue.js", function (event) { cleanTag(event.target); Queue = window.__MimicryQueue__; delete window.__MimicryQueue__; queueReady = true; checkDeferred(); }); getScriptWithTag(pathPrefix + "dependency.js", function (event) { cleanTag(event.target); Dependency = window.__MimicryDependency__; Type = Dependency.Type; delete window.__MimicryDependency__; dependencyReady = true; checkDeferred(); }); getScriptWithTag(pathPrefix + "module.js", function (event) { cleanTag(event.target); Module = window.__MimicryModule__; delete window.__MimicryModule__; moduleReady = true; checkDeferred(); }); return Mimicry; })();