UNPKG

esp-js-di

Version:

A tiny DI container (formally microdi-js)

790 lines (742 loc) 31.5 kB
(function webpackUniversalModuleDefinition(root, factory) { if(typeof exports === 'object' && typeof module === 'object') module.exports = factory(); else if(typeof define === 'function' && define.amd) define([], factory); else if(typeof exports === 'object') exports["esp-js-di"] = factory(); else root["esp-js-di"] = factory(); })(self, () => { return /******/ (() => { // webpackBootstrap /******/ "use strict"; /******/ // The require scope /******/ var __webpack_require__ = {}; /******/ /************************************************************************/ /******/ /* webpack/runtime/define property getters */ /******/ (() => { /******/ // define getter functions for harmony exports /******/ __webpack_require__.d = (exports, definition) => { /******/ for(var key in definition) { /******/ if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) { /******/ Object.defineProperty(exports, key, { enumerable: true, get: definition[key] }); /******/ } /******/ } /******/ }; /******/ })(); /******/ /******/ /* webpack/runtime/hasOwnProperty shorthand */ /******/ (() => { /******/ __webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop)) /******/ })(); /******/ /******/ /* webpack/runtime/make namespace object */ /******/ (() => { /******/ // define __esModule on exports /******/ __webpack_require__.r = (exports) => { /******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) { /******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); /******/ } /******/ Object.defineProperty(exports, '__esModule', { value: true }); /******/ }; /******/ })(); /******/ /************************************************************************/ var __webpack_exports__ = {}; // ESM COMPAT FLAG __webpack_require__.r(__webpack_exports__); // EXPORTS __webpack_require__.d(__webpack_exports__, { Container: () => (/* reexport */ Container), EspDiConsts: () => (/* reexport */ EspDiConsts), RegistrationModifier: () => (/* reexport */ RegistrationModifier), ResolverContext: () => (/* reexport */ ResolverContext), ResolverNames: () => (/* reexport */ ResolverNames), "default": () => (/* binding */ src) }); ;// ./src/utils.js /* notice_start * Copyright 2016 Dev Shop Limited * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. notice_end */ function sprintf(format, etc) { var arg = arguments; var i = 1; return format.replace(/%((%)|s)/g, function (m) { return m[2] || arg[i++]; }); } function isString(value) { return Object.prototype.toString.call(value) === '[object String]'; } function isNumber(value) { return Object.prototype.toString.call(value) === '[object Number]'; } function indexOf(array, item) { var iOf; if(typeof Array.prototype.indexOf === 'function') { iOf = Array.prototype.indexOf; } else { iOf = function(item) { var i = -1, index = -1; for(i = 0; i < this.length; i++) { if(this[i] === item) { index = i; break; } } return index; }; } var index = iOf.call(array, item); return index; } ;// ./src/resolverContext.js /* notice_start * Copyright 2016 Dev Shop Limited * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. notice_end */ // helper to track circular dependencies during a resolve call class ResolverContext { constructor() { this._isResolving = false; this._resolutionChain = []; this._hasEnded = false; } beginResolve(key) { var self = this; if(self._resolutionChain.indexOf(key) !== -1) { var resolutionChainSummary = self._resolutionChain[0]; for (var i = 1; i < self._resolutionChain.length; i++) { resolutionChainSummary += ' -required-> ' + self._resolutionChain[i]; } resolutionChainSummary += ' -required-> ' + key; throw new Error(sprintf('Circular dependency detected when resolving item by name \'%s\'.\r\nThe resolution chain was:\r\n%s', key, resolutionChainSummary)); } if(!this._isResolving) { this._isResolving = true; } self._resolutionChain.push(key); return { endResolve: function () { if(!this._hasEnded) { this._hasEnded = true; var i = self._resolutionChain.indexOf(key); if (i > -1) { self._resolutionChain.splice(i, 1); } if (self._resolutionChain.length === 0) { self._isResolving = false; } } } }; } } ;// ./src/instanceLifecycleType.js /* notice_start * Copyright 2016 Dev Shop Limited * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. notice_end */ /* harmony default export */ const instanceLifecycleType = ({ transient: 'transient', singleton: 'singleton', singletonPerContainer: 'singletonPerContainer', external: 'external' }); ;// ./src/guard.js /* notice_start * Copyright 2016 Dev Shop Limited * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. notice_end */ class Guard { static isNotNullOrUndefined(value, message) { if (typeof value === 'undefined' || value === null) { doThrow(message); } } static isFalsey(value, message) { if (value) { doThrow(message); } } static lengthIs(array, length, message) { if (array.length !== length) { doThrow(message); } } static lengthGreaterThan(array, expected, message) { if (array.length < expected) { doThrow(message); } } static lengthIsAtLeast(array, expected, message) { if (array.length < expected) { doThrow(message); } } static isString(value, message) { if (!isString(value)) { doThrow(message); } } static isNonEmptyString(value, message) { if (!isString(value) || Guard.lengthIsAtLeast(value, 1, message)) { doThrow(message); } } static isNumber(value, message) { if (!isNumber(value)) { doThrow(message); } } static isTrue(check, message) { if (!check) { doThrow(message); } } static isFunction(item, message) { if (typeof(item) != "function") { doThrow(message); } } static isObject(value,message) { if(typeof value !== 'object') { doThrow(message); } } } function doThrow(message) { if(typeof message === 'undefined' || message === '') { throw new Error('EspDi: Argument error'); } throw new Error('EspDi: ' + message); } ;// ./src/registrationModifier.js /* notice_start * Copyright 2016 Dev Shop Limited * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. notice_end */ class RegistrationModifier { constructor(registration, instanceCache, registrationGroups) { this._registration = registration; this._instanceCache = instanceCache; this._registrationGroups = registrationGroups; this.singleton(); } inject(){ this._ensureInstanceNotCreated(); var dependencyList = Array.prototype.slice.call(arguments); this._validateDependencyList(dependencyList); this._registration.dependencyList = dependencyList; return this; } transient() { this._ensureInstanceNotCreated(); this._registration.instanceLifecycleType = instanceLifecycleType.transient; return this; } singleton() { this._ensureInstanceNotCreated(); this._registration.instanceLifecycleType = instanceLifecycleType.singleton; return this; } singletonPerContainer() { this._ensureInstanceNotCreated(); this._registration.instanceLifecycleType = instanceLifecycleType.singletonPerContainer; return this; } inGroup(groupName) { Guard.isNonEmptyString(groupName, 'Error calling inGroup(groupName). The name argument must be a string and can not be \'\''); this._ensureInstanceNotCreated(); var currentContainerOwnsRegistration = true; var lookup = this._registrationGroups[groupName]; if(lookup) { // Groups are resolved against the container they are registered against. // Child containers will inherit the group unless the child overwrites the registration. currentContainerOwnsRegistration = this._registrationGroups.hasOwnProperty(groupName); } if(lookup === undefined || !currentContainerOwnsRegistration) { lookup = []; this._registrationGroups[groupName] = lookup; } if(indexOf(lookup, this._registration.name) !== -1) { throw new Error(sprintf('Instance already created for key [%s]', this._registration.name)); } lookup.push(this._registration.name); return this; } _ensureInstanceNotCreated() { if(this._registration.hasOwnProperty(this._registration.name) && this._instanceCache.hasOwnProperty(this._registration.name)) throw new Error(sprintf('Instance already created for key [%s]', this._registration.name)); } _validateDependencyList(dependencyList) { // TODO } } ;// ./src/espDiConsts.js class EspDiConsts { static get owningContainer() { return 'EspDi_OwningContainer'; } } ;// ./src/resolverNames.js class ResolverNames { static get delegate() { return 'delegate'; } static get factory() { return 'factory'; } static get externalFactory() { return 'externalFactory'; } static get literal() { return 'literal'; } } ;// ./src/isRegisteredQueryOptions.js const DefaultIsRegisteredQueryOptions = { searchParentContainers: true } ;// ./src/container.js /* notice_start * Copyright 2016 Dev Shop Limited * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. notice_end */ class Container { constructor() { this._isChildContainer = false; this._parent = undefined; this._registrations = {}; this._registrationGroups = {}; this._instanceCache = {}; this._resolverContext = new ResolverContext(); this._resolvers = this._createDefaultResolvers(); this._isDisposed = false; this._childContainers = []; this._containerEventHandlers = new Map(); this._registerSelf(); } createChildContainer() { this._throwIfDisposed(); // The child prototypically inherits some but not all props from its parent. // Below we override the ones it doesn't inherit. var child = Object.create(this); child._parent = this; child._isChildContainer = true; child._registrations = Object.create(this._registrations); child._registrationGroups = Object.create(this._registrationGroups); child._instanceCache = Object.create(this._instanceCache); child._resolvers = Object.create(this._resolvers); child._isDisposed = false; child._childContainers = []; child._registerSelf(); this._childContainers.push(child); return child; } register(name, proto) { this._throwIfDisposed(); Guard.isNonEmptyString(name, 'Error calling register(name, proto). The name argument must be a string and can not be \'\''); Guard.isNotNullOrUndefined(proto, `Error calling register(name, proto). Registered item for [${name}] can not be null or undefined`); Guard.isTrue(!isString(proto), `Error calling register(name, proto). Can not register a string instance against key [${name}], use registerInstance(name, instance)`); Guard.isTrue(!isNumber(proto), `Error calling register(name, proto). Can not register a number instance against key [${name}], use registerInstance(name, instance)`); var registration = { name: name, proto: proto, dependencyList: [], instanceLifecycleType: instanceLifecycleType.singleton }; this._registrations[name] = registration; return new RegistrationModifier(registration, this._instanceCache, this._registrationGroups); } registerFactory(name, factory) { this._throwIfDisposed(); Guard.isNonEmptyString(name, 'Error calling registerFactory(name, factory). The name argument must be a string and can not be \'\''); Guard.isFunction(factory, `Error calling registerFactory(name, factory). Provided factory for [${name}] must be a function`); let dependencyKey = { resolver: 'externalFactory', factory : factory, isResolverKey: true }; return this.register(name, dependencyKey); } registerInstance(name, instance, isExternallyOwned = true) { this._throwIfDisposed(); Guard.isNonEmptyString(name, 'Error calling register(name, instance, isExternallyOwned = true). The name argument must be a string and can not be \'\''); Guard.isNotNullOrUndefined(instance, `Error calling registerInstance(name, instance, isExternallyOwned = true). Provided instance for [${name}] can not be null or undefined`); this._registrations[name] = { name: name, instanceLifecycleType: isExternallyOwned ? instanceLifecycleType.external : instanceLifecycleType.singleton }; this._instanceCache[name] = instance; this._raiseContainerEvent('instanceRegistered', name, instance) } isRegistered(name, options = DefaultIsRegisteredQueryOptions) { this._throwIfDisposed(); Guard.isNonEmptyString(name, 'Error calling isRegistered(name). The name argument must be a string and can not be \'\''); const isRegistered = options.searchParentContainers ? !!this._registrations[name] : this._registrations.hasOwnProperty(name); return isRegistered; } isGroupRegistered(groupName, options = DefaultIsRegisteredQueryOptions) { this._throwIfDisposed(); Guard.isNonEmptyString(groupName, 'Error calling isGroupRegistered(groupName). The groupName argument must be a string and can not be \'\''); const isGroupRegistered = options.searchParentContainers ? !!this._registrationGroups[groupName] : this._registrationGroups.hasOwnProperty(groupName); return isGroupRegistered; } resolve(name, ...additionalDependencies) { this._throwIfDisposed(); Guard.isNonEmptyString(name, 'Error calling resolve(name, ...additionalDependencies). The name argument must be a string and can not be \'\''); var registration = this._registrations[name], dependency, instance, error; if (!registration) { error = sprintf('Nothing registered for dependency [%s]', name); throw new Error(error); } instance = this._tryRetrieveFromCache(name); if (!instance) { instance = this._buildInstance(name, additionalDependencies); if (registration.instanceLifecycleType === instanceLifecycleType.singleton || registration.instanceLifecycleType === instanceLifecycleType.singletonPerContainer) { this._instanceCache[name] = instance; } this._raiseContainerEvent('instanceCreated', name, instance) } else if(additionalDependencies.length > 0) { throw new Error("The provided additional dependencies can't be used to construct the instance as an existing instance was found in the container"); } return instance; } resolveGroup(groupName, ...additionalDependencies) { this._throwIfDisposed(); Guard.isNonEmptyString(groupName, 'Error calling resolveGroup(groupName). The groupName argument must be a string and can not be \'\''); var items = [], mapings, error; mapings = this._registrationGroups[groupName]; if (!mapings) { error = sprintf('No group with name [%s] registered', groupName); throw new Error(error); } for (let i = 0, len = mapings.length; i < len; i++) { items.push(this.resolve(mapings[i], ...additionalDependencies)); } return items; } addResolver(name, resolver) { this._throwIfDisposed(); Guard.isNonEmptyString(name, 'Error calling addResolver(name, resolver). The name argument must be a string and can not be \'\''); Guard.isNotNullOrUndefined(resolver, `Error calling addResolver(name, resolver). Provided resolver for [${name}] can not be null or undefined`); this._resolvers[name] = resolver; } on(eventType, eventHandler) { Guard.isNonEmptyString(eventType, 'Error calling on(eventType, eventHandler). The eventType argument must be a string and can not be \'\''); Guard.isFunction(eventHandler, 'Error calling on(eventType, eventHandler). The eventHandler argument must be a function can not be null or undefined'); let handlers = this._containerEventHandlers.get(eventType); if (!handlers) { handlers = []; this._containerEventHandlers.set(eventType, handlers); } let handlerExists = handlers.some(handler => handler === eventHandler); Guard.isFalsey(handlerExists, 'Error calling on(eventType, eventHandler). The eventHandler passed is already registered'); handlers.push(eventHandler); } off(eventType, eventHandler){ Guard.isNonEmptyString(eventType, 'Error calling off(eventType, eventHandler). The eventType argument must be a string and can not be \'\''); Guard.isFunction(eventHandler, 'Error calling off(eventType, eventHandler). The eventHandler argument must be a function can not be null or undefined'); let handlers = this._containerEventHandlers.get(eventType); if (!handlers) { return; } const removeAtIndex = handlers.indexOf(eventHandler); if (removeAtIndex > -1) { handlers.splice(removeAtIndex, 1); } } dispose() { this._disposeContainer(); } _tryRetrieveFromCache(name) { var registration = this._registrations[name], instance = this._instanceCache[name], thisContainerOwnsRegistration, thisContainerOwnsInstance, typeIsSingleton, childHasOverriddenRegistration, parentRegistrationIsSingletonPerContainer; if (this._isChildContainer) { thisContainerOwnsRegistration = this._registrations.hasOwnProperty(name); if (instance === undefined) { typeIsSingleton = registration.instanceLifecycleType === instanceLifecycleType.singleton; // do we have the right to create it, or do we need to defer to the parent? if (!thisContainerOwnsRegistration && typeIsSingleton) { // singletons always need to be resolved and stored with the container that owns the // registration, otherwise the cached instance won't live in the right place instance = this._parent.resolve(name); } } else { thisContainerOwnsInstance = this._instanceCache.hasOwnProperty(name); if (!thisContainerOwnsInstance) { childHasOverriddenRegistration = thisContainerOwnsRegistration && !thisContainerOwnsInstance; parentRegistrationIsSingletonPerContainer = !thisContainerOwnsRegistration && registration.instanceLifecycleType === instanceLifecycleType.singletonPerContainer; if (childHasOverriddenRegistration || parentRegistrationIsSingletonPerContainer) { instance = undefined; } } } } return instance; } _buildInstance(name, additionalDependencies) { var registration = this._registrations[name], dependencies = [], dependency, dependencyKey, context, instance, resolver; context = this._resolverContext.beginResolve(name); try { if (registration.dependencyList !== undefined) { for (let i = 0, len = registration.dependencyList.length; i < len; i++) { dependencyKey = registration.dependencyList[i]; if (isString(dependencyKey)) { if(this.isGroupRegistered(dependencyKey)) { dependency = this.resolveGroup(dependencyKey); } else { dependency = this.resolve(dependencyKey); } } else if (dependencyKey.hasOwnProperty('resolver') && isString(dependencyKey.resolver)) { resolver = this._resolvers[dependencyKey.resolver]; if (resolver === undefined) { throw new Error(sprintf('Error resolving [%s]. No resolver registered to resolve dependency key for resolver [%s]', name, dependencyKey.resolver)); } dependency = resolver.resolve(this, dependencyKey); } else { throw new Error(sprintf('Error resolving [%s]. It\'s dependency at index [%s] had an unknown resolver', name, i)); } dependencies.push(dependency); } } for(let j = 0, len = additionalDependencies.length; j < len; j ++) { dependencies.push(additionalDependencies[j]); } if(registration.proto.isResolverKey) { if(registration.proto.resolver) { resolver = this._resolvers[registration.proto.resolver]; instance = resolver.resolve(this, registration.proto, ...dependencies); } else { throw new Error('Registered resolverKey is missing it\'s resolver property'); } } else if (typeof registration.proto === 'function') { var Ctor = registration.proto.bind.apply( registration.proto, [null].concat(dependencies) ); instance = new Ctor(); } else { instance = Object.create(registration.proto); if (instance.init !== undefined) { instance = instance.init.apply(instance, dependencies) || instance; } } } finally { context.endResolve(); } return instance; } _createDefaultResolvers() { return { // A resolvers that delegates to the dependency keys resolve method to perform the resolution. // It expects a dependency key in format: // { resolver: 'factory', resolve: function(container) { return someInstance } } [ResolverNames.delegate]: { resolve: (container, dependencyKey) => { return dependencyKey.resolve(container); } }, // A resolvers that returns a factory that when called will resolve the dependency from the container. // Any arguments passed at runtime will be passed to resolve as additional dependencies // It expects a dependency key in format: // { resolver: 'factory', name: "aDependencyName" } [ResolverNames.factory]: { resolve: (container, dependencyKey) => { return function() { // using function here as I don't want babel to re-write the arguments var var args = [].slice.call(arguments); args.unshift(dependencyKey.key); return container.resolve.apply(container, args ); }; } }, // A resolver that invokes an external factory to resolve the dependency from the container. [ResolverNames.externalFactory]: { resolve: function(container, dependencyKey, ...additionalDeps) { return dependencyKey.factory.apply(null, [container, ...additionalDeps]); } }, // A resolver that take a literal value [ResolverNames.literal]: { resolve: function(container, dependencyKey) { Guard.isNotNullOrUndefined(dependencyKey.value, 'Invalid container configuration. A literal resolver key is missing the \'value\' property. That property should hold the value to be resolved.'); return dependencyKey.value; } } }; } _registerSelf() { // register the child with itself so any dependency that wants to resolve a container get's the container at the same scope as itself (i.e. the container that built it). this.registerInstance(EspDiConsts.owningContainer, this); } _raiseContainerEvent(eventType, name, instance) { let handlers = this._containerEventHandlers.get(eventType); if (!handlers) { return; } let notification = { name, instance, eventType }; // copy the list as we're about to call external code which could be reentrant let copy = handlers.slice(0); for (let i = 0; i < copy.length; i ++) { copy[i](notification) } } _throwIfDisposed() { if (this._isDisposed) throw new Error("Container has been disposed"); } _disposeContainer() { if (!this._isDisposed) { this._isDisposed = true; for (var prop in this._instanceCache) { if (this._instanceCache.hasOwnProperty(prop)) { var registration = this._registrations[prop]; if (registration.instanceLifecycleType !== instanceLifecycleType.external) { var instance = this._instanceCache[prop]; if (instance.dispose) { instance.dispose(); } } } } for (var i = 0, len = this._childContainers.length; i < len; i++) { var child = this._childContainers[i]; child._disposeContainer(); } } } } ;// ./src/index.js /* notice_start * Copyright 2016 Dev Shop Limited * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. notice_end */ // we export both a default object and individual items, this allows for differing import usages: // // 1) import the entire namespace // import di from 'esp-js-di'; // let container = new di.Container(); // // 2) import the entire namespace using * // import * as di from from 'esp-js-di'; // let container = new di.Container() // // 2) import single items // import { Container } from 'esp-js-di'; // let container = new Container() /* harmony default export */ const src = ({ Container: Container, RegistrationModifier: RegistrationModifier, ResolverContext: ResolverContext, EspDiConsts: EspDiConsts, ResolverNames: ResolverNames }); /******/ return __webpack_exports__; /******/ })() ; }); //# sourceMappingURL=esp-js-di.js.map