UNPKG

hinoki

Version:

sane, simple dependency injection and more

604 lines (529 loc) 14.1 kB
// this file uses type annotations http://flowtype.org/ // every function in this file is optimizable by V8 crankshaft and will // in fact get compiled and inlined after some warmup. // lint // // here are some flowtype definitions that are used throughout this file: // // # FLOWTYPE // // type Factory = (...deps: any) => any ( function (root, factory) { if (typeof define === 'function' && define.amd) { // amd define(['bluebird', 'lodash'], factory); } else if (typeof exports !== 'undefined') { // commonjs module.exports = factory( require('bluebird'), require('lodash') ); } else { // other root.hinoki = factory(root.Promise, root.lodash); } } )(this, function (Promise, _) { 'use strict'; // TODO small, optimizable functions // TODO terminology // TODO make the output easily greppable // TODO name anonymous functions // TODO benchmark with real world containers // TODO merge benchmark, optimization test and unit/integration tests // TODO predictable random numbers for test data generation // TODO _.attempt instead of try catch block // TODO try not to do any unnecessary allocations // TODO many things can just be solved with executions instead // TODO how to error handling // TODO make one huge integration benchmark file and ensure that all are optimized // TODO only if a factory returns a promise does async even happen // TODO check that all/most are inlined as well !!! // TODO a function that returns optimization state for each hinoki function // TODO preallocate arrays // TODO remove lodash dependency // TODO remove Promise dependency // this is the factory. make an exception and allow many statements: // jshint maxstatements: false // boolean function isObjectLike( // any value ) { return (value && typeof value == 'object'); } // boolean function isPromise( // any value ) { // inlined isObjectLike return (isObjectLike(value) && typeof value.then == 'function'); } var regexFunctionArguments = /\(([^\s,]+)\)/g; // Array<string> function parseFunctionArguments( // (...args: any) => any func ) { return Function.prototype.toString.call(func).match(regexFunctionArguments) || []; } // Array<string> function getFactoryDependencies( // (...args: any) => any factory ) { if (_.isArray(factory.$inject)) return factory.$inject; // inlined parseFunctionArguments return parseFunctionArguments(factory); } // TODO rename to make side effect more clear // Array<string> function getAndCacheFactoryDependencies( // (...args: any) => any factory ) { if (_.isArray(factory.$inject)) return factory.$inject; // inlined parseFunctionArguments var names = parseFunctionArguments(factory); factory.$inject = names; return names; } // boolean function arrayOfStringsHasDuplicates( // Array<string> array ) { var index = -1; var length = array.length; var seen = {}; // TODO use a set cache here while (++index < length) { var value = array[index]; // inlined _.has if (_.has(seen, value)) { return true; } seen[value] = true; } return false; } // ValueResult function ValueResult( // any value ) { this.value = value; } // FactoryResult function FactoryResult( // string name, // (...args: any) => any factory ) { this.name = name; this.factory = factory; this.nocache = false; } // type Resolver = // (name: string, container: Container) => ValueResult | FactoryResult // ? ValueResult | FactoryResult function defaultResolver( // string name, // Container container ) { // inlined isObjectLike if (!isObjectLike(container.values)) return; var value = container.values[name]; // inlined _.isUndefined and ValueResult if (!_.isUndefined(value)) return new ValueResult(value); // inlined isObjectLike if (!isObjectLike(container.factories)) return; var factory = container.factories[name]; // inlined _.isFunction if (!_.isFunction(factory)) return; // inlined getDependenciesCached and FactoryResult getDependenciesCached(factory); return new FactoryResult(name, factory); } // TODO baseFunctions // TODO is this even needed? // just make the public api stricter // // Array<T> function coerceIntoArray( value // ? Array<T> | T ) { if (_.isArray(value)) { return value; } if (!value) { return []; } return [value]; } function Container( // { [name: string]: Factory } factories, // Object<any> cache ) { this.factories = factories || {}; this.constants = constants || {}; this.cache = cache || {}; // stores promises this.unresolved = {}; this.resolvers = resolvers || []; } // Container.prototype.addResolver = function(resolver) { this.resolvers.push(resolver); } // ? ValueResult | FactoryResult function baseResolveNameInContainer( // string name, // Container container, // Array<Resolver> resolvers, // int index ) { if (resolvers.length <= index) { return defaultResolver(name, container); } resolvers[index]( name, container, function callInnerResolverFromOuterResolver(nameFromOuter) { return baseResolveNameInContainer( nameFromOuter, container, resolvers, index + 1 ); } ); } // ? ValueResult | FactoryResult function resolveNameInContainer( // string name, // Container container ) { return baseResolveNameInContainer( name, container, container.resolvers || [], 0 ); } // TODO you probably dont need this // resolve into each container after the other // ? ValueResult | FactoryResult // function resolveInContainers( // // Array<Container> // containers, // // string // name // ) { // return _.find( // containers, // function resolveInContainersFindPredicate(container) { // return resolveNameInContainer(name, container); // } // ); // } // TODO this is probably not even needed // type Result = any | Error | Promise<any, Error> // function callFactory( // // Container // container // ) { // // } // TODO ??? // Promise<any, Error> // function getPromise( // // ) { // // } // Array<string> function nameToPath( // string name ) { return [name]; } // Array<Array<string>> function namesToPaths( // Array<string> names ) { return _.map(names, nameToPath); } // polymorphic (difficult to optimize for V8) but flexible API // does very little and delegates heavy lifting // to monomorphic optimizable functions // Result function get( // Container | Array<Container> oneOrManyContainers, // string | Array<string> oneOrManyNames ) { var manyNames = _.isArray(oneOrManyNames); // most common case if (_.isArray(oneOrManyContainers)) { if (manyNames) { return baseTryContainers( namesToPaths(oneOrManyNames), oneOrManyContainers ); } else { return baseTryContainers( nameToPath(oneOrManyNames), oneOrManyContainers ); } } else { if (manyNames) { return baseGetMany( namesToPaths(oneOrManyNames), [oneOrManyContainers], 0 ); } else { return baseGetOne( nameToPath(oneOrManyNames), [oneOrManyContainers], 0 ); } } } // look up in any of the containers // any | Error | ResolvedPromise<any> | RejectedPromise<Error> function baseTryContainers( // Path path, // Array<Container> containers ) { var index = -1; var length = containers.length; while (++index < length) { var result = baseGetOne(path, containers, index); if (result) { return result; } } } if (set) { function createCache(values) { } function isCached(value) { } } // TODO use a set if possible // ? string function wouldIntroduceCycle( // Array<string> path, // Array<string> names ) { // very fast cycle detection O(path.length + names.length) var length = names.length; if (length === 0 || path.length === 0) return; var cache = createCache(path); var index = -1; while (++index < length) { var name = names[index]; if (isCached(cache, name)) return name; } } // unoptimizable because it uses try/catch function attempt( func, args ) { try { return func.apply(undefined, args); } catch(e) { return _.isError(e) ? e : new Error(e); } } // equialvalent to and faster than: // names.map(function(name) { // var newPath = path.slice(0); // path.unshift(name); // return newPath; // }) // Array<Array<string>> function getNextPaths( // Array<string> path, // Array<string> names ) { var pathLength = path.length; var namesLength = names.length; var nameIndex = -1; var nextPaths = Array(namesLength); while (++nameIndex < namesLength) { var nextPath = Array(pathLength + 1); nextPath[0] = names[nameIndex]; var pathIndex = -1; while (++pathIndex < pathLength) { nextPath[pathIndex + 1] = path[pathIndex] } } return nextPaths; } function baseCallFactory( factory, // Array<any> dependencies ) { var resultOrError = attempt(factory, dependencies); if (_.isError(resultOrError)) { // TODO first make a error in resolution error from it return resultOrError; } // TODO promise rejected error ??? return resultOrError; } // any function baseHandleFactory( // Array<string> path, // (...deps: any) => any factory, // Container containers, // int containerIndex ) { // TODO get dependencies // TODO this function is getting WAY too large: DELEGATE !!! var dependencyNames = getDependenciesCached(factory); // no dependencies if (dependencyNames.length === 0) { return callFactory(factory, []); // TODO call shared handleFactoryResult return; } var nameThatWouldIntroduceCycle = wouldIntroduceCycle(path, dependencyNames); if (nameThatWouldIntroduceCycle) { // TODO real cycle error return new Error('cycle'); } var dependencyPaths = getNextPaths(path, dependencyNames); if (dependencyNames.length === 1) { var dependencyResult = baseGetOne(dependencyPaths[1], containers, containerIndex); } var dependencyResult = baseGetMany(dependencyPaths, containers, containerIndex); // TODO handle failure in dependencyResult if (isPromise(dependencyResult)) { return dependencyResult.then(function(dependencies) { return callFactory(factory, dependencies); }); } else { return callFactory(factory, dependencyResult); } } // any | Error | ResolvedPromise<any> | RejectedPromise<Error> function baseGetOne( // Path path, // Array<Container> containers, // int containerIndex ) { // this is the only function that actually does anything significant var resultOrError = _.attempt(resolveName, path[0], containers[containerIndex]); if (_.isError(resultOrError)) { // TODO first make a error in resolution error from it return resultOrError; } if (!resultOrError) { // TODO unresolvable error // we might also just return null in that case } // TODO value result doesnt need a name property actually if (!_.isUndefined(resultOrError.value)) { return resultOrError.value; } if (!_.isFunction(resultOrError.factory)) { } if (!_.isString(resultOrError.name)) { } // TODO cache instruction ??? // TODO do we really need container here ? yes, for caching return baseHandleFactory(result.name, result.factory, container); } // Array<any> | Error | ResolvedPromise<Array<any>> | RejectedPromise<Error> function baseGetMany( // Array<Array<string>> paths, // Array<Container> containers // int containerIndex ) { var length = paths.length; var isAnyResultAPromise = false var results = Array(paths.length) var index = -1; while (++index < length) { var result = baseGetOne(path, containers, containerIndex)); // TODO put this into baseGetOne if (!result) { // TODO unresolvable error } if (_.isError(result)) { return result; } if (isPromise(result) && !isAnyResultAPromise) { isAnyResultAPromise = true; } // TODO promise merge results[index] = result; } // Promise.all(results); if (!isAnyResultAPromise) { return results; } else return Promise.all(results); } } // TODO check cycle by checking for existence before building paths // export var hinoki = {}; hinoki.isObjectLike = isObjectLike; hinoki.isPromise = isPromise; hinoki.parseFunctionArguments = parseFunctionArguments; hinoki.getDependencies = getDependencies; hinoki.getAndCacheDependencies = getAndCacheDependencies; hinoki.arrayOfStringsHasDuplicates = arrayOfStringsHasDuplicates; hinoki.resolveNameInContainer = resolveNameInContainer; hinoki.baseResolveNameInContainer = baseResolveNameInContainer; hinoki.defaultResolver = defaultResolver; hinoki.coerceIntoArray = coerceIntoArray; hinoki.ValueResult = ValueResult; hinoki.FactoryResult = FactoryResult; hinoki.Container = Container; hinoki.get = get; // reexport from lodash hinoki.isError = _.isError; return hinoki; });