factory.js (4720B)
1 import { contains } from './array.js'; 2 import { pickShallow } from './object.js'; 3 /** 4 * Create a factory function, which can be used to inject dependencies. 5 * 6 * The created functions are memoized, a consecutive call of the factory 7 * with the exact same inputs will return the same function instance. 8 * The memoized cache is exposed on `factory.cache` and can be cleared 9 * if needed. 10 * 11 * Example: 12 * 13 * const name = 'log' 14 * const dependencies = ['config', 'typed', 'divideScalar', 'Complex'] 15 * 16 * export const createLog = factory(name, dependencies, ({ typed, config, divideScalar, Complex }) => { 17 * // ... create the function log here and return it 18 * } 19 * 20 * @param {string} name Name of the function to be created 21 * @param {string[]} dependencies The names of all required dependencies 22 * @param {function} create Callback function called with an object with all dependencies 23 * @param {Object} [meta] Optional object with meta information that will be attached 24 * to the created factory function as property `meta`. 25 * @returns {function} 26 */ 27 28 export function factory(name, dependencies, create, meta) { 29 function assertAndCreate(scope) { 30 // we only pass the requested dependencies to the factory function 31 // to prevent functions to rely on dependencies that are not explicitly 32 // requested. 33 var deps = pickShallow(scope, dependencies.map(stripOptionalNotation)); 34 assertDependencies(name, dependencies, scope); 35 return create(deps); 36 } 37 38 assertAndCreate.isFactory = true; 39 assertAndCreate.fn = name; 40 assertAndCreate.dependencies = dependencies.slice().sort(); 41 42 if (meta) { 43 assertAndCreate.meta = meta; 44 } 45 46 return assertAndCreate; 47 } 48 /** 49 * Sort all factories such that when loading in order, the dependencies are resolved. 50 * 51 * @param {Array} factories 52 * @returns {Array} Returns a new array with the sorted factories. 53 */ 54 55 export function sortFactories(factories) { 56 var factoriesByName = {}; 57 factories.forEach(factory => { 58 factoriesByName[factory.fn] = factory; 59 }); 60 61 function containsDependency(factory, dependency) { 62 // TODO: detect circular references 63 if (isFactory(factory)) { 64 if (contains(factory.dependencies, dependency.fn || dependency.name)) { 65 return true; 66 } 67 68 if (factory.dependencies.some(d => containsDependency(factoriesByName[d], dependency))) { 69 return true; 70 } 71 } 72 73 return false; 74 } 75 76 var sorted = []; 77 78 function addFactory(factory) { 79 var index = 0; 80 81 while (index < sorted.length && !containsDependency(sorted[index], factory)) { 82 index++; 83 } 84 85 sorted.splice(index, 0, factory); 86 } // sort regular factory functions 87 88 89 factories.filter(isFactory).forEach(addFactory); // sort legacy factory functions AFTER the regular factory functions 90 91 factories.filter(factory => !isFactory(factory)).forEach(addFactory); 92 return sorted; 93 } // TODO: comment or cleanup if unused in the end 94 95 export function create(factories) { 96 var scope = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; 97 sortFactories(factories).forEach(factory => factory(scope)); 98 return scope; 99 } 100 /** 101 * Test whether an object is a factory. This is the case when it has 102 * properties name, dependencies, and a function create. 103 * @param {*} obj 104 * @returns {boolean} 105 */ 106 107 export function isFactory(obj) { 108 return typeof obj === 'function' && typeof obj.fn === 'string' && Array.isArray(obj.dependencies); 109 } 110 /** 111 * Assert that all dependencies of a list with dependencies are available in the provided scope. 112 * 113 * Will throw an exception when there are dependencies missing. 114 * 115 * @param {string} name Name for the function to be created. Used to generate a useful error message 116 * @param {string[]} dependencies 117 * @param {Object} scope 118 */ 119 120 export function assertDependencies(name, dependencies, scope) { 121 var allDefined = dependencies.filter(dependency => !isOptionalDependency(dependency)) // filter optionals 122 .every(dependency => scope[dependency] !== undefined); 123 124 if (!allDefined) { 125 var missingDependencies = dependencies.filter(dependency => scope[dependency] === undefined); // TODO: create a custom error class for this, a MathjsError or something like that 126 127 throw new Error("Cannot create function \"".concat(name, "\", ") + "some dependencies are missing: ".concat(missingDependencies.map(d => "\"".concat(d, "\"")).join(', '), ".")); 128 } 129 } 130 export function isOptionalDependency(dependency) { 131 return dependency && dependency[0] === '?'; 132 } 133 export function stripOptionalNotation(dependency) { 134 return dependency && dependency[0] === '?' ? dependency.slice(1) : dependency; 135 }