modules/app-engine/main.js

!function AppEngineWrapper() {
  // eslint-disable-next-line no-extend-native
  String.prototype.endsWith = function AppEngineEndWith(suffix) {
    return this.indexOf(suffix, this.length - suffix.length) !== -1;
  };
  let core; let mod; let o = {}; const pipelines = function() {};

  /* let Service; let log; const services = {}; let config; let util; const loadMessages = {}; */


  /**
   * Blackrock AppEngine Module
   *
   * @public
   * @class Server.Modules.AppEngine
   * @augments Server.Modules.Core.Module
   * @param {Server.Modules.Core} coreObj - The Core Module Singleton
   * @return {Server.Modules.AppEngine} module - The AppEngine Module Singleton
   *
   * @description This is the AppEngine Module of the Blackrock Application Server.
   * It loads all apps and the controllers and other files within them and manages
   * access to all of these files - in tandem with the Router Module - from any
   * Interface. It also exposes a Swagger 2.0 Compliant API Definition file for each
   * of your apps (can be toggled on/off in config), which can easily be paired up
   * with SwaggerUI in your app's html folder for quick and easy documentation and
   * testing.
   *
   * @author Darren Smith
   * @copyright Copyright (c) 2021 Darren Smith
   * @license Licensed under the LGPL license.
   */
  module.exports = function AppEngineModule(coreObj) {
    if (mod) return mod;
    core = coreObj; mod = new core.Mod('AppEngine');
    // noinspection JSUnresolvedFunction
    core.module.isLoaded('utilities').then(function AppEngineModuleIsUtilLoadedThen(utilMod) {
      o.util = utilMod;
    }).catch(function AppEngineModuleIsUtilLoadedCatch(err) {
      core.shutdown('' + JSON.parse(err));
    });
    o.log = core.module('logger').log; o.config = core.cfg();
    o.apps = {}; o.loadMessages = {};
    o.log('debug', 'AppEngine > Initialising...',
        {module: mod.name}, 'MODULE_INIT');
    o.App = new core.Mod().extend({constructor: function AppEngineAppBuilder() {}});
    process.nextTick(function() {
      pipelines.init();
    });
    return mod;
  };


  /**
   * (Internal > Pipeline [1]) Initialisation Pipeline
   *
   * @private
   * @memberof Server.Modules.AppEngine
   * @function pipelines.init
   * @ignore
   *
   * @description
   * Tbc...
   *
   * @example
   * Tbc...
   */
  pipelines.init = function AppEngineInitPipeline() {
        // noinspection JSUnresolvedFunction
        core.lib.rxPipeline({}).pipe(

            // Fires once on server initialisation:
            pipelines.init.bindUnloadMethod,
            pipelines.init.bindSearchMethod,
            pipelines.init.bindAppEndpoint,
            pipelines.init.bindSupportMethods,
            pipelines.init.loadApps,

            // Fires once per loaded app:
            pipelines.init.fetchControllerNames,
            pipelines.init.preProcessControllers,
            pipelines.init.removeInvalidControllers,
            pipelines.init.setBasePathCtrl,
            pipelines.init.waitThenExposeAppDef,
            pipelines.init.generateControllerEvents,

            // Fires once per controller within each loaded app:
            pipelines.init.loadControllerFiles,
            pipelines.init.setBasePathAndPattern,
            pipelines.init.checkIfWildcardPath,
            pipelines.init.buildRoutesObject,
            pipelines.init.findRouters,
            pipelines.init.setGetFn,
            pipelines.init.buildAppRoutes

        ).subscribe();
  };


  /**
   * (Internal > Pipeline [2]) Search Pipeline
   *
   * @private
   * @memberof Server.Modules.AppEngine
   * @function pipelines.search
   * @ignore
   *
   * @description
   * Tbc...
   *
   * @example
   * Tbc...
   */
  pipelines.search = function AppEngineSearchPipeline(event, cb) {
    // noinspection JSUnresolvedFunction
    core.lib.rxPipeline(event).pipe(

        // Fires once per app route search query:
        pipelines.search.parseSearchObject,
        pipelines.search.setupHosts,
        pipelines.search.generateAppEvents,
        pipelines.search.initSearchForApp,
        pipelines.search.iterateOverRoutes,
        pipelines.search.checkAndMatch

    ).subscribe(function AppEngineSPLSubscribe(result) {
      cb(result);
    });
  };


  /**
   * (Internal > Stream Methods [1]) Bind Unload Method
   *
   * @private
   * @memberof Server.Modules.AppEngine
   * @function pipelines.init.bindUnloadMethod
   * @ignore
   * @param {observable} source - The Source Observable
   * @return {observable} destination - The Destination Observable
   *
   * @description
   * Tbc...
   *
   * @example
   * Tbc...
   */
  pipelines.init.bindUnloadMethod = function AppEngineIPLBindUnloadMethod(source) {
    // noinspection JSUnresolvedFunction
    return core.lib.rxOperator(function AppEngineIPLBindUnloadMethod(observer, evt) {
      /**
       * Unload The AppEngine Module
       *
       * @private
       * @memberof Server.Modules.AppEngine
       * @function unload
       * @ignore
       *
       * @description
       * Tbc...
       *
       * @example
       * Tbc...
       */
      mod.unload = function AppEngineUnload() {
        const closeControllers = function AppEngineUnloadCloseCtrl(cb) {
          let ctrlCount = 0; let counter = 0;
          if (o.apps) {
            for (const app in o.apps) {
              // noinspection JSUnfilteredForInLoop
              if (o.apps[app].routes && o.apps[app].routes.length > 0) {
                // noinspection JSUnfilteredForInLoop
                ctrlCount += o.apps[app].routes.length;
              }
            }
          }
          if (ctrlCount === 0) {
            cb();
            return;
          }
          for (const app in o.apps) {
            // noinspection JSUnfilteredForInLoop
            if (o.apps[app].routes && o.apps[app].routes.length > 0) {
              // noinspection JSUnfilteredForInLoop
              for (let i = 0; i < o.apps[app].routes.length; i++) {
                // noinspection JSUnfilteredForInLoop
                const route = o.apps[app].routes[i];
                if (!route.controller.shutdown || !(route.controller.shutdown instanceof Function)) {
                  // noinspection JSUnfilteredForInLoop
                  o.log('debug_deep',
                      'AppEngine > Attempting to shutdown controller (' + route.pattern + ') for the app ' +
                      o.apps[app].cfg.name + '  but no shutdown interface exists.', {module: mod.name},
                      'APPENGINE_SHUTDOWN_CTRL_NO_INT');
                  counter ++;
                } else {
                  // noinspection JSUnfilteredForInLoop
                  o.log('debug_deep',
                      'AppEngine > Attempting to shutdown controller (' + route.pattern + ') for app ' +
                      o.apps[app].cfg.name + ', waiting for controller response...',
                      {module: mod.name}, 'APPENGINE_SHUTDOWN_CTRL_WAITING');
                  route.controller.shutdown(function AppEngineUnloadShutdownCb() {
                    // noinspection JSUnfilteredForInLoop
                    o.log('debug',
                        'AppEngine > Controller ' + route.pattern + ' for app ' + o.apps[app].cfg.name +
                        ' shutdown successful.', {module: mod.name}, 'APPENGINE_SHUTDOWN_CTRL_SUCCESS');
                    counter ++;
                  });
                }
              }
            }
          }
          const timeout = 5000; let timeoutTimer = 0;
          const interval = setInterval(function AppEngineUnloadCloseCtrlTimeout() {
            if (counter >= ctrlCount) {
              o.log('shutdown',
                  'AppEngine > Controllers all shutdown where possible.',
                  {module: mod.name}, 'APPENGINE_SHUTDOWN_CTRL_COMPLETE');
              clearInterval(interval);
              cb();
              return;
            }
            if (timeoutTimer > timeout) {
              o.log('shutdown',
                  'AppEngine > Controller shutdown timed out.',
                  {module: mod.name}, 'APPENGINE_SHUTDOWN_CTRL_TIMEOUT');
              clearInterval(interval);
              cb();
              return;
            }
            timeoutTimer += 500;
          }, 500);
        };
        closeControllers(function AppEngineUnloadCloseCtrlCb() {
          core.emit('module-shut-down', 'AppEngine');
        });
      };
      o.log('debug', 'AppEngine > [1] Attached \'unload\' Method To This Module',
          {module: mod.name}, 'APPENGINE_UNLOAD_BOUND');
      observer.next(evt);
    }, source);
  };

  /**
   * (Internal > Stream Methods [2]) Bind Search Method
   *
   * @private
   * @memberof Server.Modules.AppEngine
   * @function pipelines.init.bindSearchMethod
   * @ignore
   * @param {observable} source - The Source Observable
   * @return {observable} destination - The Destination Observable
   *
   * @description
   * Tbc...
   *
   * @example
   * Tbc...
   */
  pipelines.init.bindSearchMethod = function AppEngineIPLBindSearch(source) {
    // noinspection JSUnresolvedFunction
    return core.lib.rxOperator(function AppEngineIPLBindSearchOp(observer, evt) {
      /**
       * (External) Searches for a Controller
       *
       * @public
       * @memberof Server.Modules.AppEngine
       * @function search
       * @param {object} searchObj - Search Definition Object
       * @param {function} cb - Callback Function
       *
       * @description
       * Tbc...
       *
       * @example
       * {
       *   apps: ["app1", "app2"],
       *   hostname: "localhost",
       *   url: "/web/users/1"
       * }
       */
      mod.search = function AppEngineIPLBindSearchSearch(searchObj, cb) {
        pipelines.search({'searchObj': searchObj}, function(evt) {
          cb(evt.result);
        });
      };
      o.log('debug', 'AppEngine > [2] Attached \'search\' Method To This Module',
          {module: mod.name}, 'APPENGINE_SEARCH_BOUND');
      observer.next(evt);
    }, source);
  };

  /**
   * (Internal > Stream Methods [3]) Bind App Endpoint
   *
   * @private
   * @memberof Server.Modules.AppEngine
   * @function pipelines.init.bindAppEndpoint
   * @ignore
   * @param {observable} source - The Source Observable
   * @return {observable} destination - The Destination Observable
   *
   * @description
   * Tbc...
   *
   * @example
   * Tbc...
   */
  pipelines.init.bindAppEndpoint = function AppEngineIPLBindAppEP(source) {
    // noinspection JSUnresolvedFunction
    return core.lib.rxOperator(function AppEngineIPLBindAppEPOp(observer, evt) {
      // noinspection JSValidateTypes
      /**
       * (Internal) Middleware Router
       *
       * @private
       * @memberof Server.Modules.AppEngine
       * @function MiddlewareRouter
       * @ignore
       * @param {object} req - Request Object
       * @param {object} res - Response Object
       *
       * @description
       * Tbc...
       *
       * @example
       * Tbc...
       */
      evt.MiddlewareRouter = new core.Base().extend({
        constructor: function AppEngineIPLMiddlewareRouter() {
          const self = this;
          self.myRouter = function(req, res, handler) {
            let stackCounter = 0;
            const innerRouter = function AppEngineIPLMiddlewareInnerRouter(req, res, handler) {
              if (self.myRouter.stack[stackCounter]) {
                self.myRouter.stack[stackCounter](req, res, function() {
                  stackCounter++;
                  if (self.myRouter.stack[stackCounter]) {
                    innerRouter(req, res, handler);
                  } else {
                    handler(req, res);
                    stackCounter = 0;
                  }
                });
              } else {
                handler(req, res);
                stackCounter = 0;
              }
            };
            return innerRouter(req, res, handler);
          };
          self.myRouter.stack = [];
          self.myRouter.use = function AppEngineIPLMiddlewareRouterUse(fn) {
            self.myRouter.stack.push(fn); return true;
          };
          self.myRouter.count = function AppEngineIPLMiddlewareRouterCount() {
            return self.myRouter.stack.length;
          };
        },
      });

      // noinspection JSUnfilteredForInLoop
      /**
       * (External) Access App Instance
       *
       * @public
       * @class Server.Modules.AppEngine.app
       * @memberof Server.Modules.AppEngine
       * @param {string} name - App Name
       * @return {Server.Modules.AppEngine.app | Object} appObj - App Instance
       *
       * @description
       * Tbc...
       *
       * @example
       * Tbc...
       */
      mod.app = function AppEngineGetApp(name) {
        if (!name) return {};
        const generateAppObject = function AppEngineGenerateAppObject() {
          if (!o.apps[name]) return;
          const app = function() {};
          /**
           * (External) Get App Config
           *
           * @public
           * @memberof Server.Modules.AppEngine.app
           * @name cfg
           * @function
           * @return {object} config - Config Object
           *
           * @description
           * Tbc...
           *
           * @example
           * Tbc...
           */
          app.cfg = function AppEngineAppCfg() {
            return o.apps[name].cfg;
          };
          /**
           * (External) App Models Object
           *
           * @public
           * @class Server.Modules.AppEngine.app.models
           * @memberof Server.Modules.AppEngine.app
           * @name models
           *
           * @description
           * Tbc...
           *
           * @example
           * Tbc...
           */
          app.models = function() {};
          /**
           * (External) Get App Model
           *
           * @public
           * @memberof Server.Modules.AppEngine.app.models
           * @name get
           * @function
           * @param {object} modelName - Model
           * @return {object} modelObject - App Model
           *
           * @description
           * Tbc...
           *
           * @example
           * Tbc...
           */
          app.models.get = function AppEngineAppGetModel(modelName) {
            return o.apps[name].models[modelName];
          };
          /**
           * (External) Add App Model
           *
           * @public
           * @memberof Server.Modules.AppEngine.app.models
           * @name add
           * @function
           * @param {object} modelName - Model
           * @param {object} modelObject - App Model
           * @return {boolean} result - Result (True | False)
           *
           * @description
           * Tbc...
           *
           * @example
           * Tbc...
           */
          app.models.add = function AppEngineAppAddModel(modelName, modelObject) {
            if (!name || !modelName || !modelObject) return false;
            if (!o.apps[name].models) o.apps[name].models = {};
            o.apps[name].models[modelName] = modelObject;
            return true;
          };
          /**
           * (External) App URL Object
           *
           * @public
           * @class Server.Modules.AppEngine.app.url
           * @memberof Server.Modules.AppEngine.app
           * @name url
           *
           * @description
           * Tbc...
           *
           * @example
           * Tbc...
           */
          app.url = {};
          /**
           * (External) Get App URL
           *
           * @public
           * @memberof Server.Modules.AppEngine.app.url
           * @name get
           * @function
           * @param {string} path - Path
           * @param {object} options - App Model
           * @return {string} url - URL to App
           *
           * @description
           * Tbc...
           *
           * @example
           * Tbc...
           */
          app.url.get = function AppEngineAppGetUrl(path, options) {
            let protocol; let port; let basePath; let portString;
            if (options && options.protocol)protocol = options.protocol.toLowerCase();
            if (options && options.port) port = options.port;
            const host = o.apps[name].cfg.host;
            if (o.apps[name].cfg.basePath) basePath = o.apps[name].cfg.basePath;
            else if (core.cfg().core && core.cfg().core.basePath) basePath = core.cfg().core.basePath;
            else basePath = '';
            // noinspection JSUnresolvedVariable
            if (options && options.full === true) {
              if (!protocol) protocol = 'http';
              if (protocol === 'http' && port && port !== 80 && port !== 0) portString = ':' + port;
              else if (protocol === 'https' && port && port !== 443 && port !== 0) portString = ':' + port;
              else portString = '';
              return protocol + '://' + host + portString + basePath + path;
            } else {
              return basePath + path;
            }
          };
          /**
           * (External) App Variables Object
           *
           * @public
           * @class Server.Modules.AppEngine.app.vars
           * @memberof Server.Modules.AppEngine.app
           * @name vars
           *
           * @description
           * Tbc...
           *
           * @example
           * Tbc...
           */
          app.vars = function() {};
          /**
           * (External) Get App Variable
           *
           * @public
           * @memberof Server.Modules.AppEngine.app.vars
           * @name get
           * @function
           * @param {string} key - App Variable Key
           * @return {*} value - App Variable Value
           *
           * @description
           * Tbc...
           *
           * @example
           * Tbc...
           */
          app.vars.get = function AppEngineAppGetVar(key) {
            return o.apps[name].vars[key];
          };
          /**
           * (External) Set App Variable
           *
           * @public
           * @memberof Server.Modules.AppEngine.app.vars
           * @name set
           * @function
           * @param {string} key - App Variable Key
           * @param {object} value - App Variable Value
           * @return {boolean} result - Result of Value Update
           *
           * @description
           * Tbc...
           *
           * @example
           * Tbc...
           */
          app.vars.set = function AppEngineAppSetVar(key, value) {
            o.apps[name].vars[key] = value;
            return true;
          };
          /**
           * (Undocumented) App Middleware
           *
           * @private
           * @memberof Server.Modules.AppEngine.app
           * @name middleware
           * @ignore
           *
           * @description
           * Tbc...
           *
           * @example
           * Tbc...
           */
          const middleware = app.middleware = o.apps[name].middleware;
          /**
           * (External) Use Middleware
           *
           * @public
           * @memberof Server.Modules.AppEngine.app
           * @name use
           * @function
           * @param {function} middleware - Middleware Function
           *
           * @description
           * Tbc...
           *
           * @example
           * Tbc...
           */
          app.use = middleware.use;
          if (o.apps[name].obj) {
            /**
             * (External) App Routes Object
             *
             * @public
             * @memberof Server.Modules.AppEngine.app
             * @name routes
             * @function
             *
             * @description
             * Tbc...
             *
             * @example
             * Tbc...
             */
            app.routes = function() {};
            for (const objName in o.apps[name].obj) {
              // noinspection JSUnfilteredForInLoop
              if (!app.routes[objName] && objName) {
                // noinspection JSUnfilteredForInLoop
                app.routes[objName] = o.apps[name].obj[objName];
              }
            }
          }
          // noinspection JSUnresolvedVariable
          if (o.config['app-engine'].runtime.controllers.allowLoad === true) {
            /**
             * (External) Load App Controller
             *
             * @public
             * @memberof Server.Modules.AppEngine.app
             * @name loadController
             * @function
             * @param {string} path - Path to App Controller to Load
             * @param {object} ctrl - App Controller to Load
             * @return {boolean} result - Result of Loading Controller (True | False)
             *
             * @description
             * Tbc...
             *
             * @example
             * Tbc...
             */
            app.loadController = function AppEngineAppLoadCtrl(path, ctrl) {
              if (!path || !(typeof path === 'string' || path instanceof String)) return false;
              if ((typeof ctrl !== 'object' && ctrl === null) || !ctrl) return false;
              if (!o.apps[name]) return false;
              o.apps[name].map[path] = o.apps[name].routes.push({
                path: '',
                pattern: path,
                controller: ctrl,
                app: name,
              }) - 1;
              return true;
            };
          }
          // noinspection JSUnresolvedVariable
          if (o.config['app-engine'].runtime.controllers.allowUnload === true) {
            /**
             * (External) Unload App Controller
             *
             * @public
             * @memberof Server.Modules.AppEngine.app
             * @name unloadController
             * @function
             * @param {string} path - Path to App Controller to Load
             * @return {boolean} result - Result of Unloading Controller (True | False)
             *
             * @description
             * Tbc...
             *
             * @example
             * Tbc...
             */
            app.unloadController = function AppEngineAppUnloadCtrl(path) {
              if (o.apps[name].routes[o.apps[name].map[path]]) {
                delete o.apps[name].routes[o.apps[name].map[path]];
                delete o.apps[name].map[path];
                return true;
              } else return false;
            };
          }
          return app;
        };
        return generateAppObject();
      };
      o.log('debug',
          'AppEngine > [3] Setup & Attached \'app\' Method To This Module (incl. Setting Up Middleware)',
          {module: mod.name}, 'APPENGINE_APP_BOUND');
      observer.next(evt);
    }, source);
  };

  /**
   * (Internal > Stream Methods [4]) Bind Support Methods
   *
   * @private
   * @memberof Server.Modules.AppEngine
   * @function pipelines.init.bindSupportMethods
   * @ignore
   * @param {observable} source - The Source Observable
   * @return {observable} destination - The Destination Observable
   *
   * @description
   * Tbc...
   *
   * @example
   * Tbc...
   */
  pipelines.init.bindSupportMethods = function AppEngineIPLBindSupportFns(source) {
    // noinspection JSUnresolvedFunction
    return core.lib.rxOperator(function AppEngineIPLBindSupportFnsOp(observer, evt) {
      o.log('debug', 'AppEngine > [4] Binding Support Methods',
          {module: mod.name}, 'APPENGINE_SUPPORT_FNS_BOUND');
      /**
       * (External) Gets An App Object (By Name)
       *
       * @public
       * @memberof Server.Modules.AppEngine
       * @name appStats
       * @function
       * @param {string} name - App Name
       * @return {object} appStatsObj - App Stats Object
       *
       * @description
       * Tbc...
       *
       * @example
       * Tbc...
       */
      mod.appStats = function AppEngineAppStats(name) {
        const stats = {};
        stats.appsRouteCount = 0;
        if (!name) {
          stats.appsCount = Object.keys(o.apps).length;
          if (core.module('utilities')) {
            stats.appsMemoryUse = core.module('utilities').system.getObjectMemoryUsage(o.apps);
          }
        } else {
          if (o.apps[name]) {
            stats.appsCount = 1;
            if (core.module('utilities')) {
              stats.appsMemoryUse = core.module('utilities').system.getObjectMemoryUsage(o.apps[name]);
            }
          } else {
            stats.appsCount = 0;
            stats.appsMemoryUse = 0;
          }
        }
        stats.apps = {};
        // noinspection JSUnfilteredForInLoop
        for (const app in o.apps) {
          if ((name && app === name) || !name) {
            // noinspection JSUnfilteredForInLoop
            stats.apps[app] = {};
            if (core.module('utilities')) {
              // noinspection JSUnfilteredForInLoop
              stats.apps[app].appMemoryUse =
                core.module('utilities').system.getObjectMemoryUsage(o.apps[app]);
            }
            // noinspection JSUnfilteredForInLoop
            if (o.apps[app].routes) {
              // noinspection JSUnfilteredForInLoop
              stats.apps[app].appRouteCount = o.apps[app].routes.length;
              // noinspection JSUnfilteredForInLoop
              stats.appsRouteCount += o.apps[app].routes.length;
            }
          }
        }
        return stats;
      };
      /**
       * (External) Returns a List of Loaded App Names
       *
       * @public
       * @memberof Server.Modules.AppEngine
       * @name list
       * @function
       * @return {array} apps - List of App Names
       *
       * @description
       * Tbc...
       *
       * @example
       * Tbc...
       */
      mod.list = function AppEngineIPLAppList() {
        return Object.keys(o.apps);
      };
      // noinspection JSUnresolvedVariable
      if (o.config['app-engine'].runtime.apps.allowUnload === true) {
        /**
         * (External) Unload App
         *
         * @public
         * @memberof Server.Modules.AppEngine
         * @name unloadApp
         * @function
         * @param {string} name - App Name to Unload
         * @return {boolean} result - Result of Unload
         *
         * @description
         * Tbc...
         *
         * @example
         * Tbc...
         */
        mod.unloadApp = function AppEngineIPLUnloadApp(name) {
          if (o.apps[name]) {
            delete o.apps[name];
            return true;
          } else return false;
        };
      }
      // noinspection JSUnresolvedVariable
      if (o.config['app-engine'].runtime.apps.allowLoad === true) {
        /**
         * (External) Reload App
         *
         * @public
         * @memberof Server.Modules.AppEngine
         * @name reloadApp
         * @function
         * @param {string} name - App Name to Reload
         * @return {boolean} result - Result of Reload
         *
         * @description
         * Tbc...
         *
         * @example
         * Tbc...
         */
        mod.reloadApp = function AppEngineReloadApp(name) {
          if (o.apps[name]) {
            delete o.apps[name];
            // noinspection JSUnresolvedFunction
            mod.load(name).then(function(app){}).catch(function(err){});
            return true;
          } else return false;
        };
      }
      observer.next(evt);
    }, source);
  };

  /**
   * (Internal > Stream Methods [5]) Load Apps
   *
   * @private
   * @memberof Server.Modules.AppEngine
   * @function pipelines.init.loadApps
   * @ignore
   * @param {observable} source - The Source Observable
   * @return {observable} destination - The Destination Observable
   *
   * @description
   * Tbc...
   *
   * @example
   * Tbc...
   */
  pipelines.init.loadApps = function AppEngineIPLLoadApps(source) {
    // noinspection JSUnresolvedFunction
    return core.lib.rxOperator(function AppEngineIPLLoadAppsOp(observer, evt) {
      o.log('startup', 'AppEngine > [5] Enumerating and loading apps...',
          {}, 'APPENGINE_ENUM_APPS');
      /**
       * (External > Load App Method
       *
       * @public
       * @memberof Server.Modules.AppEngine
       * @function load
       * @param {string} appName - The Name of the App to Load
       * @param {object} [appCfg] - The App Configuration Object
       * @param {string} [group] - The Relative Path Of The Filesystem Group The App Is In
       * @return {Promise} Promise - Promise That Must Be Listened To (Then + Catch)
       *
       * @todo Allow loadApp() method to load apps from remote URLs and from specific paths
       *
       * @description
       * Tbc...
       *
       * @example
       * Tbc...
       */
      const loadApp = function AppEngineIPLLoadApp(appName, appCfg, group) {
        // eslint-disable-next-line no-undef
        return new Promise(function(resolve, reject) {
          let appExistsInFilesystem = false; let appIsRemote = false; let appGroupName;
          if (group && require('fs').existsSync(core.fetchBasePath('apps') + '/' +
            group + '/' + appName + '/app.json') === true) {
            appExistsInFilesystem = true;
            appGroupName = group;
          } else if (!group && require('fs').existsSync(core.fetchBasePath('apps') +
            '/' + appName + '/app.json') === true) {
            appExistsInFilesystem = true;
          } else if (appName.includes('.')) appIsRemote = true;
          if (!appExistsInFilesystem && !appIsRemote && !appCfg) {
            o.log('startup',
                'AppEngine > [5a] Failed to load app (' + appName + ') - Invalid App',
                {module: mod.name, app: appName}, 'APPENGINE_INVALID_APP');
            // eslint-disable-next-line prefer-promise-reject-errors
            reject('Invalid App (' + appName + ')');
            return false;
          }
          const setupApp = function AppEngineIPLSetupApp(appName, appFilesystem, appRemote, appConfig, appGroup) {
            if (appConfig.active) {
              o.log('startup',
                  'AppEngine > [5a] Loading ' + appName + ' app...',
                  {module: mod.name, app: appName}, 'APPENGINE_LOADING_APP');
              o.apps[appName] = new mod.app();
              o.apps[appName].cfg = appConfig;
              o.apps[appName].routes = [];
              o.apps[appName].map = {};
              o.apps[appName].orphans = {};
              o.apps[appName].vars = {};
              const middlewareRouter = new evt.MiddlewareRouter();
              // noinspection JSUnresolvedVariable
              o.apps[appName].middleware = middlewareRouter.myRouter;
              if (appRemote) o.apps[appName].remote = true;
              if (appFilesystem || appRemote) {
                const event = {app: appName};
                if (appGroup) event.appGroup = appGroup;
                if (appRemote) event.remotePaths = appRemote.paths;
                if (appFilesystem) event.filesystemApp = true;
                process.nextTick(function AppEngineIPLLoadAppsNextTickCb() {
                  observer.next(event);
                });
              }
              resolve(mod.app(appName));
            } else {
              // eslint-disable-next-line prefer-promise-reject-errors
              reject({'message': 'App Inactive'});
            }
          };
          if (!appCfg && appExistsInFilesystem && appGroupName) {
            appCfg = require(core.fetchBasePath('apps') + '/' +
              appGroupName + '/' + appName + '/app.json');
            appCfg.appPath = core.fetchBasePath('apps') + '/' + appGroupName + '/' + appName;
            setupApp(appName, true, false, appCfg, appGroupName);
          } else if (!appCfg && appExistsInFilesystem && !appGroupName) {
            appCfg = require(core.fetchBasePath('apps') + '/' + appName + '/app.json');
            appCfg.appPath = core.fetchBasePath('apps') + '/' + appName;
            setupApp(appName, true, false, appCfg);
          } else if (!appCfg && !appExistsInFilesystem && appIsRemote) {
            core.module('http', 'interface').client.get(appName + '/app.json', function(err, res) {
              if (err) {
                // eslint-disable-next-line prefer-promise-reject-errors
                reject({'message': 'Remote App Invalid', 'err': err});
                return;
              }
              appCfg = {
                'name': res.data.info.title,
                'host': res.data.host,
                'basePath': res.data.basePath,
                'active': true,
                'exposeDefinition': false,
              };
              setupApp(appName, false, res.data, appCfg);
            });
          } else {
            setupApp(appName, false, false, appCfg);
          }
        });
      };
      // noinspection JSUnresolvedVariable
      if (o.config['app-engine'].runtime.apps.allowLoad === true) {
        mod.load = loadApp;
      }
      const loadAppsFromFilesystem = function AppEngineIPLLoadFromFS(appBasePath, appGroup) {
        if (require('fs').existsSync(appBasePath) && require('fs').lstatSync(appBasePath).isDirectory()) {
          require('fs').readdirSync(appBasePath).forEach(
              function AppEngineIPLLoadAppReadDirCb(file) {
                if (require('fs').existsSync(appBasePath + '/' + file + '/app.json') === true) {
                  loadApp(file, null, appGroup).then(function(app) {
                  }).catch(function() {});
                } else if (file !== '.DS_Store') {
                  let newFile;
                  if (appGroup) newFile = appGroup + '/' + file;
                  else newFile = file;
                  loadAppsFromFilesystem(appBasePath + '/' + file, newFile);
                }
              });
        }
      };
      loadAppsFromFilesystem(core.fetchBasePath('apps'));
    }, source);
  };


  /**
   * (Internal > Stream Methods [6]) Fetch Controller Names
   *
   * @private
   * @memberof Server.Modules.AppEngine
   * @function pipelines.init.fetchControllerNames
   * @ignore
   * @param {observable} source - The Source Observable
   * @return {observable} destination - The Destination Observable
   *
   * @description
   * Tbc...
   *
   * @example
   * Tbc...
   */
  pipelines.init.fetchControllerNames = function AppEngineIPLFetchCtrlNames(source) {
    // noinspection JSUnresolvedFunction
    return core.lib.rxOperator(function AppEngineIPLFetchCtrlNamesOp(observer, evt) {
      o.log('startup',
          'AppEngine > [6] Building routes for ' + evt.app + ' app.',
          {module: mod.name}, 'APPENGINE_BUILDING_ROUTES');
      if (o.apps[evt.app].routes[0]) {
        o.log('startup',
            'AppEngine > [6a] App Routes for ' + evt.app + ' Have Already Been Built',
            {module: mod.name}, 'APPENGINE_ROUTES_ALREADY_BUILT');
        return;
      }
      // noinspection JSUnresolvedVariable
      if (((o.apps[evt.app].cfg.routing && o.apps[evt.app].cfg.routing === 'auto') ||
        !o.apps[evt.app].cfg.routing) && evt.filesystemApp) {
        let pathToWalk;
        if (evt.appGroup) {
          pathToWalk = core.fetchBasePath('apps') + '/' + evt.appGroup + '/' + evt.app + '/controllers';
        } else {
          pathToWalk = core.fetchBasePath('apps') + '/' + evt.app + '/controllers';
        }
        require('./_support/filewalker.js')(pathToWalk, function AppEngineIPLFetchCtrlNamesFilewalkerCb(err, data) {
          evt.fileWalkerErr = err;
          evt.data = data;
          observer.next(evt);
        });
      }
      if (evt.remotePaths) observer.next(evt);
    }, source);
  };

  /**
   * (Internal > Stream Methods [7]) Pre-Process Controllers
   *
   * @private
   * @memberof Server.Modules.AppEngine
   * @function pipelines.init.preProcessControllers
   * @ignore
   * @param {observable} source - The Source Observable
   * @return {observable} destination - The Destination Observable
   *
   * @description
   * Tbc...
   *
   * @example
   * Tbc...
   */
  pipelines.init.preProcessControllers = function AppEngineIPLPreProcessCtrl(source) {
    // noinspection JSUnresolvedFunction
    return core.lib.rxOperator(function AppEngineIPLPreProcessCtrlOp(observer, evt) {
      o.log('debug',
          'AppEngine > [7] Controllers Are Being Pre-Processed (' + evt.app + ')',
          {module: mod.name}, 'APPENGINE_CTRLS_PREPROCESSED');
      if (evt.fileWalkerErr) throw evt.fileWalkerErr;
      evt.dataPreProcess = evt.data;
      if (!evt.data && !evt.remotePaths) {
        return {success: false, message: 'No controllers exist for the ' + evt.app + ' app.'};
      }
      if (evt.data) {
        evt.data = []; evt.basePath = [];
        for (let i = 0; i < evt.dataPreProcess.length; i++) {
          if (!evt.dataPreProcess[i].endsWith('.DS_Store')) evt.data.push(evt.dataPreProcess[i]);
          if (evt.dataPreProcess[i].endsWith('controller.js')) {
            evt.basePath.push(evt.dataPreProcess[i].substring(0, evt.dataPreProcess[i].length - 14));
          }
        }
        evt.basePath.sort(function AppEngineIPLPreProcessCtrlSortHandler(a, b) {
          return a.length - b.length || a.localeCompare(b);
        });
        evt.controllerBasePath = evt.basePath[0];
      }
      observer.next(evt);
    }, source);
  };

  /**
   * (Internal > Stream Methods [8]) Remove Invalid Controllers
   *
   * @private
   * @memberof Server.Modules.AppEngine
   * @function pipelines.init.removeInvalidControllers
   * @ignore
   * @param {observable} source - The Source Observable
   * @return {observable} destination - The Destination Observable
   *
   * @description
   * Tbc...
   *
   * @example
   * Tbc...
   */
  pipelines.init.removeInvalidControllers = function AppEngineIPLRemoveInvalidCtrl(source) {
    // noinspection JSUnresolvedFunction
    return core.lib.rxOperator(function AppEngineIPLRemoveInvalidCtrlOp(observer, evt) {
      if (evt.data) {
        for (let i = 0; i < evt.data.length; i++) {
          evt.data[i] = evt.data[i].replace(evt.controllerBasePath, '');
          if (evt.data[i].endsWith('controller.js') && evt.data[i] !== '/controller.js') {
            delete evt.data[i];
          } else if (evt.data[i].endsWith('controller.js') && evt.data[i] === '/controller.js') {
            evt.data[i] = '/';
          }
        }
      }
      o.log('debug',
          'AppEngine > [8] Invalid Controllers Have Been Removed (' + evt.app + ')',
          {module: mod.name}, 'APPENGINE_INVALID_CTRLS_REMOVED');
      observer.next(evt);
    }, source);
  };

  /**
   * (Internal > Stream Methods [9]) Set Base Path Controller
   *
   * @private
   * @memberof Server.Modules.AppEngine
   * @function pipelines.init.setBasePathCtrl
   * @ignore
   * @param {observable} source - The Source Observable
   * @return {observable} destination - The Destination Observable
   *
   * @description
   * Tbc...
   *
   * @example
   * Tbc...
   */
  pipelines.init.setBasePathCtrl = function AppEngineIPLSetBasePathCtrl(source) {
    // noinspection JSUnresolvedFunction
    return core.lib.rxOperator(function AppEngineIPLSetBasePathCtrlOp(observer, evt) {
      if (o.apps[evt.app].cfg.basePath) {
        const pathBits = o.apps[evt.app].cfg.basePath.split('/');
        const pathBitsCount = pathBits.length;
        for (let i = 0; i < pathBitsCount - 1; i++) {
          let urlsPiece;
          if (i === 0 && pathBits[i] === '') urlsPiece = '/' + pathBits[0];
          else urlsPiece = '';
          for (let j = 1; j <= i; j++) {
            urlsPiece += '/' + pathBits[j];
          }
          const ctrl = {
            'get': function AppEngineIPLBasePathCtrlGet(req, res) {
              res.redirect(o.apps[evt.app].cfg.basePath);
            },
          };
          o.apps[evt.app].map[urlsPiece] = o.apps[evt.app].routes.push({
            path: '',
            pattern: urlsPiece,
            controller: ctrl,
            metadata: {
              'get': {
                'openapi': {
                  'summary': 'Root Redirect',
                  'description': 'Root Redirect for Base Path Controller',
                  'responses': {
                    '302': {},
                  },
                },
              },
            },
            app: evt.app,
          }) - 1;
        }
      }
      o.log('debug',
          'AppEngine > [9] Have Set Base Path Controller & App Routes',
          {module: mod.name}, 'APPENGINE_BASE_PATH_CTRL_SET');
      observer.next(evt);
    }, source);
  };

  /**
   * (Internal > Stream Methods [9.5]) Wait Then Expose App Definition
   *
   * @private
   * @memberof Server.Modules.AppEngine
   * @function pipelines.init.waitThenExposeAppDef
   * @ignore
   * @param {observable} source - The Source Observable
   * @return {observable} destination - The Destination Observable
   *
   * @description
   * Tbc...
   *
   * @example
   * Tbc...
   */
  pipelines.init.waitThenExposeAppDef = function AppEngineIPLWaitThenExposeAppDef(source) {
    // noinspection JSUnresolvedFunction
    return core.lib.rxOperator(function AppEngineIPLWaitThenExposeAppDefOp(observer, evt) {
      o.log('debug',
          'AppEngine > [9.5] Waiting for Routes To Be Built Then Exposing App Definition Endpoint',
          {module: mod.name}, 'APPENGINE_WAIT_EXPOSE_DEF');
      o.apps[evt.app].loadedCtrlCount = 0;
      o.apps[evt.app].totalCtrlCount = 0;
      if (evt.data) {
        for (let i = 0; i < evt.data.length; i++) {
          if (evt.data[i]) o.apps[evt.app].totalCtrlCount ++;
        }
      }
      if (evt.remotePaths) {
        for (let i = 0; i < evt.remotePaths.length; i++) {
          if (evt.remotePaths[i]) o.apps[evt.app].totalCtrlCount ++;
        }
      }
      let timer = 0; const timeout = 5000;
      const interval = setInterval(function AppEngineIPLWaitThenExposeIntervalCb() {
        if (o.apps[evt.app].loadedCtrlCount >= o.apps[evt.app].totalCtrlCount) {
          clearInterval(interval);
          const ctrl = {
            'get': function AppEngineIPLAppDefCtrlGet(req, res) {
              if (o.apps[evt.app].cfg.exposeDefinition === true) {
                // noinspection JSUnresolvedVariable
                const json = {
                  'swagger': '2.0',
                  'info': {
                    'title': o.apps[evt.app].cfg.name,
                    'description': o.apps[evt.app].cfg.description,
                    'version': o.apps[evt.app].cfg.version,
                    'app': o.apps[evt.app].cfg.termsUrl,
                    'contact': {},
                    'license': {},
                  },
                  'host': o.apps[evt.app].cfg.host,
                  'basePath': o.apps[evt.app].cfg.basePath,
                  'schemes': [
                    'http',
                    'https',
                  ],
                  'paths': {},
                };
                if (o.apps[evt.app].cfg.license) {
                  json.info.license.name = o.apps[evt.app].cfg.license.name;
                  json.info.license.url = o.apps[evt.app].cfg.license.url;
                }
                // noinspection JSUnresolvedVariable
                if (o.apps[evt.app].cfg.author) {
                  // noinspection JSUnresolvedVariable
                  json.info.contact.email = o.apps[evt.app].cfg.author.email;
                }
                // noinspection JSUnresolvedVariable
                if (core.cfg().interfaces.http.http.port !== 80) {
                  // noinspection JSUnresolvedVariable
                  json.host += ':' + core.cfg().interfaces.http.http.port;
                }
                const routes = o.apps[evt.app].routes;
                for (let i = 0; i < routes.length; i++) {
                  json.paths[routes[i].pattern] = {};
                  if (routes[i].controller.get && (!routes[i].metadata ||
                    !routes[i].metadata.get || !routes[i].metadata.get.openapi)) {
                    json.paths[routes[i].pattern].get = {
                      'summary': 'Gets list or details of an object',
                      'description': '',
                      'responses': {
                        '200': {},
                      },
                    };
                  } else if (routes[i].controller.get && routes[i].metadata &&
                    routes[i].metadata.get && routes[i].metadata.get.openapi) {
                    // noinspection JSUnresolvedVariable
                    if (!routes[i].metadata.get.general || !routes[i].metadata.get.general.hide ||
                      routes[i].metadata.get.general.hide !== true) {
                      json.paths[routes[i].pattern].get = routes[i].metadata.get.openapi;
                    }
                  }
                  if (routes[i].controller.post && (!routes[i].metadata ||
                    !routes[i].metadata.post || !routes[i].metadata.post.openapi)) {
                    json.paths[routes[i].pattern].post = {
                      'summary': 'Creates a new object',
                      'description': '',
                      'responses': {
                        '200': {},
                      },
                    };
                  } else if (routes[i].controller.post && routes[i].metadata &&
                    routes[i].metadata.post && routes[i].metadata.post.openapi) {
                    // noinspection JSUnresolvedVariable
                    if (!routes[i].metadata.post.general || !routes[i].metadata.post.general.hide ||
                      routes[i].metadata.post.general.hide !== true) {
                      json.paths[routes[i].pattern].post = routes[i].metadata.post.openapi;
                    }
                  }
                  if (routes[i].controller.put && (!routes[i].metadata ||
                    !routes[i].metadata.put || !routes[i].metadata.put.openapi)) {
                    json.paths[routes[i].pattern].put = {
                      'summary': 'Updates an object',
                      'description': '',
                      'responses': {
                        '200': {},
                      },
                    };
                  } else if (routes[i].controller.put && routes[i].metadata &&
                    routes[i].metadata.put && routes[i].metadata.put.openapi) {
                    // noinspection JSUnresolvedVariable
                    if (!routes[i].metadata.put.general || !routes[i].metadata.put.general.hide ||
                      routes[i].metadata.put.general.hide !== true) {
                      json.paths[routes[i].pattern].put = routes[i].metadata.put.openapi;
                    }
                  }
                  if (routes[i].controller.delete && (!routes[i].metadata ||
                    !routes[i].metadata.delete || !routes[i].metadata.delete.openapi)) {
                    json.paths[routes[i].pattern].delete = {
                      'summary': 'Deletes an object',
                      'description': '',
                      'responses': {
                        '200': {},
                      },
                    };
                  } else if (routes[i].controller.delete && routes[i].metadata &&
                    routes[i].metadata.delete && routes[i].metadata.delete.openapi) {
                    // noinspection JSUnresolvedVariable
                    if (!routes[i].metadata.delete.general || !routes[i].metadata.delete.general.hide ||
                      routes[i].metadata.delete.general.hide !== true) {
                      json.paths[routes[i].pattern].delete = routes[i].metadata.delete.openapi;
                    }
                  }
                  if (Object.keys(json.paths[routes[i].pattern]).length === 0) {
                    delete json.paths[routes[i].pattern];
                  }
                }
                res.send(json);
              } else {
                res.send({});
              }
            },
          };
          const pattern = '/app.json';
          o.apps[evt.app].map[pattern] = o.apps[evt.app].routes.push({
            path: '',
            pattern: pattern,
            controller: ctrl,
            metadata: {
              'get': {
                'openapi': {
                  'summary': 'App Definition Endpoint',
                  'description': 'App Definition Endpoint (Swagger/OpenAPI Compliant)',
                  'produces': ['application/json'],
                  'tags': ['API'],
                  'responses': {
                    '200': {},
                  },
                },
              },
            },
            app: evt.app,
          }) - 1;
        }
        if (timer >= timeout) clearInterval(interval);
        timer += 10;
      }, 10);
      observer.next(evt);
    }, source);
  };

  /**
   * (Internal > Stream Methods [10]) Generate Controller Events
   *
   * @private
   * @memberof Server.Modules.AppEngine
   * @function pipelines.init.generateControllerEvents
   * @ignore
   * @param {observable} source - The Source Observable
   * @return {observable} destination - The Destination Observable
   *
   * @description
   * Tbc...
   *
   * @example
   * Tbc...
   */
  pipelines.init.generateControllerEvents = function AppEngineIPLGenerateCtrlEvts(source) {
    // noinspection JSUnresolvedFunction
    return core.lib.rxOperator(function AppEngineIPLGenerateCtrlEvtsOp(observer, evt) {
      o.log('debug', 'AppEngine > [10] Controller Events Being Generated',
          {module: mod.name}, 'APPENGINE_CTRL_EVTS_GEN');
      if (evt.data) {
        for (let i = 0; i < evt.data.length; i++) {
          if (evt.data[i]) {
            o.apps[evt.app].loadedCtrlCount ++;
            evt.path = evt.controllerBasePath + evt.data[i] + '/controller.js';
            evt.path = evt.path.replace('//', '/');
            evt.i = i;
            observer.next(evt);
          }
        }
      } else if (evt.remotePaths) {
        for (let i = 0; i < evt.remotePaths.length; i++) {
          evt.i = i;
        }
      }
    }, source);
  };


  /**
   * (Internal > Stream Methods [11]) Load Controller Files in to Memory
   *
   * @private
   * @memberof Server.Modules.AppEngine
   * @function pipelines.init.loadControllerFiles
   * @ignore
   * @param {observable} source - The Source Observable
   * @return {observable} destination - The Destination Observable
   *
   * @description
   * Tbc...
   *
   * @example
   * Tbc...
   */
  pipelines.init.loadControllerFiles = function AppEngineIPLLoadCtrlFiles(source) {
    // noinspection JSUnresolvedFunction
    return core.lib.rxOperator(function AppEngineIPLLoadCtrlFilesOp(observer, evt) {
      let type; let typeString;
      if (o.config['app-engine'].sandbox.default === true) {
        type = 'sandbox';
        typeString = 'Via Sandbox';
      } else {
        type = 'require';
        typeString = 'Direct';
      }
      if (!o.loadMessages['1-10']) {
        o.loadMessages['1-10'] = true;
        o.log('debug',
            'AppEngine > [11] Loading Controller Files In To Memory (' + typeString + ')',
            {module: mod.name}, 'APPENGINE_CTRL_FILES_LOADING');
      }
      const fs = require('fs');
      if (evt.path && fs.existsSync(evt.path)) {
        const prepareCoreProxy = function AppEngineIPLPrepCoreProxy(event) {
          const app = event.app;
          if (event.controller.init && typeof event.controller.init === 'function') {
            if (event.controller.init.length >= 1) {
              let coreProxy;
              /* if (o.util.prop(o.config, 'app-engine.allow')) */ coreProxy = core.getCoreProxy(o.config['app-engine'].allow, app);
              /* else coreProxy = {}; */
              event.controller.init(coreProxy);
            } else event.controller.init();
            observer.next(event);
          } else observer.next(event);
        };
        if (type === 'sandbox') {
          const event = evt;
          core.module('sandbox').execute({'file': evt.path, 'i': evt.i, 'app': evt.app},
              function AppEngineIPLSandboxExecCb(obj) {
                event.controller = obj.ctrl;
                event.path = obj.file;
                event.i = obj.i;
                event.app = obj.app;
                prepareCoreProxy(event);
                observer.next(event);
              });
        } else {
          evt.controller = require(evt.path);
          const metaDataPath = evt.path.slice(0, -13) + 'metadata.json';
          if (fs.existsSync(metaDataPath)) {
            try {
              evt.metadata = require(metaDataPath);
            } catch (err) {
              evt.metadata = null;
            }
          } else evt.metadata = null;
          prepareCoreProxy(evt);
        }
      } else {
        evt.controller = {};
        observer.next(evt);
      }
    }, source);
  };

  /**
   * (Internal > Stream Methods [12]) Set Base Path & Pattern
   *
   * @private
   * @memberof Server.Modules.AppEngine
   * @function pipelines.init.setBasePathAndPattern
   * @ignore
   * @param {observable} source - The Source Observable
   * @return {observable} destination - The Destination Observable
   *
   * @description
   * Tbc...
   *
   * @example
   * Tbc...
   */
  pipelines.init.setBasePathAndPattern = function AppEngineIPLSetBasePathAndPattern(source) {
    // noinspection JSUnresolvedFunction
    return core.lib.rxOperator(function AppEngineIPLSetBasePathAndPatternOp(observer, evt) {
      if (!o.loadMessages['1-11']) {
        o.loadMessages['1-11'] = true;
        o.log('debug',
            'AppEngine > [12] Base Path & Pattern Being Set Now',
            {module: mod.name}, 'APPENGINE_BASE_PATH_PATTERN_SET');
      }
      if (!o.apps[evt.app].cfg.basePath && core.cfg().core && core.cfg().core.basePath) {
        o.apps[evt.app].cfg.basePath = core.cfg().core.basePath;
      }
      if (o.apps[evt.app].cfg.basePath) evt.pattern = '' + o.apps[evt.app].cfg.basePath + evt.data[evt.i];
      else evt.pattern = '' + evt.data[evt.i];
      observer.next(evt);
    }, source);
  };

  /**
   * (Internal > Stream Methods [13]) Check If Wildcard Path
   *
   * @private
   * @memberof Server.Modules.AppEngine
   * @function pipelines.init.checkIfWildcardPath
   * @ignore
   * @param {observable} source - The Source Observable
   * @return {observable} destination - The Destination Observable
   *
   * @description
   * Tbc...
   *
   * @example
   * Tbc...
   */
  pipelines.init.checkIfWildcardPath = function AppEngineIPLCheckIfWildcardPath(source) {
    // noinspection JSUnresolvedFunction
    return core.lib.rxOperator(function AppEngineIPLCheckIfWildcardPathOp(observer, evt) {
      if (evt.pattern.endsWith('{*}')) {
        const parentPattern = evt.pattern.slice(0, -3);
        if (!o.apps[evt.app].map[parentPattern] &&
            !o.apps[evt.app].routes[o.apps[evt.app].map[parentPattern]]) {
          o.apps[evt.app].orphans[parentPattern] = true;
        }
        if (!o.apps[evt.app].routes[o.apps[evt.app].map[parentPattern]]) {
          o.apps[evt.app].routes[o.apps[evt.app].map[parentPattern]] = {};
        }
        if (o.apps[evt.app].routes[o.apps[evt.app].map[parentPattern]]) {
          o.apps[evt.app].routes[o.apps[evt.app].map[parentPattern]].wildcard = true; evt.pattern = null;
        }
        if (o.apps[evt.app].map[parentPattern]) {
          o.apps[evt.app].routes[o.apps[evt.app].map[parentPattern]].wildcard = true; evt.pattern = null;
        }
      }
      if (!o.loadMessages['1-12']) {
        o.loadMessages['1-12'] = true;
        o.log('debug', 'AppEngine > [13] Checked If Wildcard Path',
            {module: mod.name}, 'APPENGINE_WILDCARD_PATH_CHECKED');
      }
      observer.next(evt);
    }, source);
  };

  /**
   * (Internal > Stream Methods [14]) Build Routes Object
   *
   * @private
   * @memberof Server.Modules.AppEngine
   * @function pipelines.init.buildRoutesObject
   * @ignore
   * @param {observable} source - The Source Observable
   * @return {observable} destination - The Destination Observable
   *
   * @description
   * Tbc...
   *
   * @example
   * Tbc...
   */
  pipelines.init.buildRoutesObject = function AppEngineIPLBuildRoutesObj(source) {
    // noinspection JSUnresolvedFunction
    return core.lib.rxOperator(function AppEngineIPLBuildRoutesObjOp(observer, evt) {
      if (evt.pattern) {
        const routeObject = {
          path: evt.path,
          pattern: evt.pattern,
          controller: evt.controller,
          metadata: evt.metadata,
          app: evt.app,
        };
        if (o.apps[evt.app].orphans[evt.pattern]) routeObject.wildcard = true;
        o.apps[evt.app].map[evt.pattern] = o.apps[evt.app].routes.push(routeObject) - 1;
        evt.routeObject = routeObject;
      }
      if (!o.loadMessages['1-13']) {
        o.loadMessages['1-13'] = true;
        o.log('debug', 'AppEngine > [14] Routes Added To Routes Object',
            {module: mod.name}, 'APPENGINE_BUILT_ROUTES_OBJ');
      }
      observer.next(evt);
    }, source);
  };

  /**
   * (Internal > Stream Methods [15]) Find & Set Routers
   *
   * @private
   * @memberof Server.Modules.AppEngine
   * @function pipelines.init.findRouters
   * @ignore
   * @param {observable} source - The Source Observable
   * @return {observable} destination - The Destination Observable
   *
   * @description
   * Tbc...
   *
   * @example
   * Tbc...
   */
  pipelines.init.findRouters = function AppEngineIPLFindRouters(source) {
    // noinspection JSUnresolvedFunction
    return core.lib.rxOperator(function AppEngineIPLFindRoutersOp(observer, evt) {
      const routers = [];
      for (const routerName in o.config.router.instances) {
        // noinspection JSUnfilteredForInLoop
        if (o.config.router.instances[routerName].interfaces &&
          o.config.router.instances[routerName].interfaces.includes('*') &&
          o.config.router.instances[routerName].apps &&
          (o.config.router.instances[routerName].apps.includes('*') ||
          o.config.router.instances[routerName].apps.includes(evt.app))) {
          // noinspection JSUnfilteredForInLoop
          routers.push(core.module('router').get(routerName));
        }
      }
      if (routers[0]) evt.router = routers[0];
      else evt.router = null;
      observer.next(evt);
      if (!o.loadMessages['1-14']) {
        o.loadMessages['1-14'] = true;
        o.log('debug', 'AppEngine > [15] Finding & Setting Router',
            {module: mod.name}, 'APPENGINE_FIND_ROUTERS');
      }
    }, source);
  };

  /**
   * (Internal > Stream Methods [16]) Set GetFn Method
   *
   * @private
   * @memberof Server.Modules.AppEngine
   * @function pipelines.init.setGetFn
   * @ignore
   * @param {observable} source - The Source Observable
   * @return {observable} destination - The Destination Observable
   *
   * @description
   * Tbc...
   *
   * @example
   * Tbc...
   * */
  pipelines.init.setGetFn = function AppEngineIPLSetGetFn(source) {
    // noinspection JSUnresolvedFunction
    return core.lib.rxOperator(function AppEngineIPLSetGetFnOp(observer, evt) {
      if (evt.pattern) {
        evt.splitPattern = evt.routeObject.pattern.split('/');
        if (!o.apps[evt.app].obj) o.apps[evt.app].obj = {'params': {}};
        evt.appPointer = evt.origAppPointer = o.apps[evt.app].obj;
        evt.getFn = function(par, cur, pat) {
          return function(a, b, c) {
            if (par.params) this[cur].params = par.params;
            else this[cur].params = {};
            let theTag;
            this[cur].pattern = pat;
            if (cur && cur.startsWith('{')) {
              if (!a) a = '';
              const param = cur.substring(1, cur.length - 1);
              this[cur].params[param] = a;
              this[cur].thisName = par.thisName;
              this[cur].pattern += this.pattern;
            } else this[cur].thisName = cur;
            this[cur].parent = par;
            for (const tag in par[cur]) {
              // noinspection JSUnfilteredForInLoop
              if (tag && tag.startsWith('{')) {
                // noinspection JSUnfilteredForInLoop
                theTag = tag.substring(1, tag.length - 1);
              }
            }
            if (theTag && (typeof a === 'string' || typeof a === 'number') && !b && !c) {
              return this[cur]['{' + theTag + '}'](a);
            } else if (!theTag && (typeof a === 'string' || typeof a === 'number') && !b && !c) {
              return this[cur];
            } else if (!a && !b && !c) {
              return this[cur];
            } else {
              let inputObj;
              if (typeof a === 'object' && a !== null) inputObj = a;
              else if (typeof b === 'object' && b !== null) inputObj = b;
              let callbackFn;
              if (typeof b === 'function') callbackFn = b;
              else if (typeof c === 'function') callbackFn = c;
              if (evt.router) {
                this[cur].path = this[cur].pattern;
                // eslint-disable-next-line guard-for-in
                for (const param in this[cur].params) {
                  // noinspection JSUnfilteredForInLoop
                  this[cur].path = this[cur].path.replace('{' + param + '}', this[cur].params[param]);
                }
                this[cur].host = core.module('app-engine').app(evt.app).cfg().host;
                const msgId = core.module('utilities').uuid4();
                const theMessage = {
                  'type': 'apps', 'msgId': msgId, 'state': 'incoming', 'directional': 'request/response',
                  'request': {
                    'path': this[cur].path, 'host': this[cur].host, 'query': inputObj.query,
                    'headers': inputObj.headers, 'params': this[cur].params, 'internal': inputObj.internal,
                    'verb': inputObj.method, 'body': inputObj.body, 'cookies': inputObj.cookies,
                  },
                };
                const responseListener = function AppEngineIPLRouteReqResListener(msg) {
                  o.log('debug',
                      'AppEngine > Received Incoming Request From Router:',
                      {module: mod.name, message: msg}, 'APPENGINE_RECEIVED_REQ');
                  mod.removeListener('outgoing.' + msg.msgId, responseListener);
                  if (callbackFn) callbackFn(null, msg.response);
                };
                mod.on('outgoing.' + theMessage.msgId, responseListener);
                evt.router.incoming(theMessage);
              }
            }
          };
        };
      }
      observer.next(evt);
      if (!o.loadMessages['1-15']) {
        o.loadMessages['1-15'] = true;
        o.log('debug', 'AppEngine > [16] Setting GetFn Method',
            {module: mod.name}, 'APPENGINE_SET_GET_FN');
      }
    }, source);
  };

  /**
   * (Internal > Stream Methods [17]) Build App Routes
   *
   * @private
   * @memberof Server.Modules.AppEngine
   * @function pipelines.init.buildAppRoutes
   * @ignore
   * @param {observable} source - The Source Observable
   * @return {observable} destination - The Destination Observable
   *
   * @description
   * Tbc...
   *
   * @example
   * Tbc...
   */
  pipelines.init.buildAppRoutes = function AppEngineIPLBuildAppRoutes(source) {
    // noinspection JSUnresolvedFunction
    return core.lib.rxOperator(function AppEngineIPLBuildAppRoutesOp(observer, evt) {
      if (evt.pattern) {
        let pattern = '';
        for (let i = 0; i < evt.splitPattern.length; i++) {
          let current = evt.splitPattern[i];
          if (i !== 0) {
            pattern = pattern + '/' + current;
            const parent = evt.appPointer;
            if (evt.appPointer && current && !evt.appPointer[current]) {
              if (!current) current = '';
              evt.appPointer[current] = evt.getFn(parent, current, pattern);
            }
            if (evt.appPointer[current]) evt.appPointer = evt.appPointer[current];
          }
        }
      }
      o.apps[evt.app].loadedCtrlCount ++;
      observer.next(evt);
      if (!o.loadMessages['1-16']) {
        o.loadMessages['1-16'] = true;
        o.log('debug', 'AppEngine > [17] Building app Routes',
            {module: mod.name}, 'APPENGINE_BUILD_APP_ROUTES');
      }
    }, source);
  };


  /**
   * (Internal > Stream Methods [1]) Parse Search Object
   *
   * @private
   * @memberof Server.Modules.AppEngine
   * @function pipelines.search.parseSearchObject
   * @ignore
   * @param {observable} source - The Source Observable
   * @return {observable} destination - The Destination Observable
   *
   * @description
   * Tbc...
   *
   * @example
   * Tbc...
   */
  pipelines.search.parseSearchObject = function AppEngineSPLParseSearchObj(source) {
    // noinspection JSUnresolvedFunction
    return core.lib.rxOperator(function AppEngineSPLParseSearchObjOp(observer, evt) {
      if (!evt.searchObj) {
        evt.searchComplete = true;
      } else if (evt.searchObj.hostname && evt.searchObj.url && !evt.searchObj.apps) {
        evt.hostname = evt.searchObj.hostname; evt.url = evt.searchObj.url;
      } else if (evt.searchObj.hostname && evt.searchObj.url && evt.searchObj.apps) {
        if (evt.searchObj.apps.includes('*')) {
          evt.hostname = evt.searchObj.hostname; evt.url = evt.searchObj.url;
        } else {
          evt.hostname = evt.searchObj.hostname; evt.url = evt.searchObj.url; evt.apps = evt.searchObj.apps;
        }
      } else evt.searchComplete = true;
      o.log('debug', 'AppEngine > [1] Search object has been parsed',
          {module: mod.name}, 'APPENGINE_SEARCH_OBJ_PARSED');
      observer.next(evt);
    }, source);
  };

  /**
   * (Internal > Stream Methods [2]) Setup Hosts
   *
   * @private
   * @memberof Server.Modules.AppEngine
   * @function pipelines.search.setupHosts
   * @ignore
   * @param {observable} source - The Source Observable
   * @return {observable} destination - The Destination Observable
   *
   * @description
   * Tbc...
   *
   * @example
   * Tbc...
   */
  pipelines.search.setupHosts = function AppEngineSPLSetupHosts(source) {
    // noinspection JSUnresolvedFunction
    return core.lib.rxOperator(function AppEngineSPLSetupHostsOp(observer, evt) {
      evt.results = []; evt.hosts = [];
      for (const app in o.apps) {
        // noinspection JSUnfilteredForInLoop
        if (!o.apps[app].cfg.host && core.cfg().core && core.cfg().core.host) {
          // noinspection JSUnfilteredForInLoop
          o.apps[app].cfg.host = core.cfg().core.host;
          // noinspection JSUnfilteredForInLoop
          evt.hosts.push(o.apps[app].cfg.host);
        }
      }
      o.log('debug', 'AppEngine > [2] Hosts Have Been Setup',
          {module: mod.name}, 'APPENGINE_HOSTS_SETUP');
      observer.next(evt);
    }, source);
  };

  /**
   * (Internal > Stream Methods [3]) Generate App Events
   *
   * @private
   * @memberof Server.Modules.AppEngine
   * @function pipelines.search.generateAppEvents
   * @ignore
   * @param {observable} source - The Source Observable
   * @return {observable} destination - The Destination Observable
   *
   * @description
   * Tbc...
   *
   * @example
   * Tbc...
   */
  pipelines.search.generateAppEvents = function AppEngineSPLGenAppEvts(source) {
    // noinspection JSUnresolvedFunction
    return core.lib.rxOperator(function AppEngineSPLGenAppEvtsOp(observer, evt) {
      o.log('debug', 'AppEngine > [3] Generating App Events...',
          {module: mod.name}, 'APPENGINE_GEN_APP_EVTS');
      const hostname = evt.hostname; const url = evt.url;
      const eApps = evt.apps; const hosts = evt.hosts; let evt2 = {};
      // eslint-disable-next-line guard-for-in
      for (const app in o.apps) {
        evt2 = {hostname: hostname, url: url, apps: eApps, hosts: hosts};
        // noinspection JSUnfilteredForInLoop
        if (hostname === o.apps[app].cfg.host) {
          evt2.app = app;
          break;
        } else { // noinspection JSUnfilteredForInLoop
          if (o.apps[app].cfg.host === '*' && !evt.hosts.includes(hostname)) {
            evt2.app = app;
            break;
          }
        }
      }
      if (evt2.app) observer.next(evt2);
      else observer.next(evt);
    }, source);
  };

  /**
   * (Internal > Stream Methods [4]) Initialise The Search For This App
   *
   * @private
   * @memberof Server.Modules.AppEngine
   * @function pipelines.search.initSearchForApp
   * @ignore
   * @param {observable} source - The Source Observable
   * @return {observable} destination - The Destination Observable
   *
   * @description
   * Tbc...
   *
   * @example
   * Tbc...
   */
  pipelines.search.initSearchForApp = function AppEngineSPLInitSearchForApp(source) {
    // noinspection JSUnresolvedFunction
    return core.lib.rxOperator(function AppEngineSPLInitSearchForAppOp(observer, evt) {
      if (!o.apps[evt.app]) {
        observer.next(evt);
        return;
      }
      evt.urlParts = evt.url.split('/');
      evt.param = {}; evt.currentRoute = null; evt.override = false; evt.routes = {};
      evt.routes[o.apps[evt.app].cfg.host] = o.apps[evt.app].routes;
      evt.directMatch = false; evt.wildcardSet = null;
      if (evt.routes[evt.hostname]) evt.host = evt.hostname;
      else if (evt.routes['*'] && !evt.routes[evt.hostname] && o.apps[evt.app].cfg.host === '*') {
        evt.host = '*';
      }
      o.log('debug',
          'AppEngine > [4] Search has been initialised for this app (' + evt.app + ')',
          {module: mod.name}, 'APPENGINE_SEARCH_INIT');
      observer.next(evt);
    }, source);
  };

  /**
   * (Internal > Stream Methods [5]) Iterate Over Routes
   *
   * @private
   * @memberof Server.Modules.AppEngine
   * @function pipelines.search.iterateOverRoutes
   * @ignore
   * @param {observable} source - The Source Observable
   * @return {observable} destination - The Destination Observable
   *
   * @description
   * Tbc...
   *
   * @example
   * Tbc...
   */
  pipelines.search.iterateOverRoutes = function AppEngineSPLIterateOverRoutes(source) {
    // noinspection JSUnresolvedFunction
    return core.lib.rxOperator(function AppEngineSPLIterateOverRoutesOp(observer, evt) {
      if (!evt) observer.next({});
      o.log('debug', 'AppEngine > [5] Iterating Over App Routes',
          {module: mod.name}, 'APPENGINE_ITERATING_OVER_ROUTES');
      if (!evt || !evt.routes || !evt.routes[evt.host]) observer.next(evt);
      const processIteration = function AppEngineSPLIterateOverRoutesProcess(index) {
        evt.match = true;
        const patternSplit = evt.routes[evt.host][index].pattern.split('/');
        if (evt.urlParts.length === patternSplit.length ||
          (evt.url.startsWith(evt.routes[evt.host][index].pattern) && evt.routes[evt.host][index].wildcard)) {
          if (evt.routes[evt.host][index].wildcard) evt.wildcardSet = {'host': evt.host, 'index': index};
          if (evt.url === evt.routes[evt.host][index].pattern) {
            evt.directMatch = true;
            evt.override = evt.routes[evt.host][index];
            if (evt.host === '*') evt.override.matchType = 'wildcard';
            else evt.override.matchType = 'direct';
          }
          if (!evt.directMatch) {
            const patternReplaced = evt.routes[evt.host][index].pattern.replace(/{.*}/, '{}');
            const patternReplacedSplit = patternReplaced.split('/');
            if (evt.urlParts.length === patternReplacedSplit.length) {
              let patternReplacedMatch = true;
              for (let i = 0; i < evt.urlParts.length; i++) {
                if (evt.urlParts[i] !== patternReplacedSplit[i] && patternReplacedSplit[i] !== '{}') {
                  patternReplacedMatch = false;
                }
              }
              if (patternReplacedMatch) {
                evt.override = evt.routes[evt.host][index];
                if (evt.host === '*') evt.override.matchType = 'wildcard';
                else evt.override.matchType = 'direct';
              }
            }
          }
          for (let i = 0, l = evt.urlParts.length; i < l; i++) {
            let workaround = false;
            if (!patternSplit[i]) {
              workaround = true;
              patternSplit[i] = '';
            }
            const reg = patternSplit[i].match(/{(.*)}/);
            if (reg) evt.param[reg[1]] = evt.urlParts[i];
            else {
              if (patternSplit[i] !== evt.urlParts[i] && !evt.wildcardSet) {
                evt.match = false; break;
              }
            }
          }
        } else {
          if (!evt.currentRoute) evt.match = false;
        }
        if (evt.match === true && !evt.currentRoute && !evt.override) {
          evt.currentRoute = evt.routes[evt.host][index];
          if (evt.host === '*') evt.currentRoute.matchType = 'wildcard';
          else evt.currentRoute.matchType = 'direct';
        }
      };
      let index; let total;
      if (evt && evt.routes && evt.routes[evt.host]) {
        for (index = 0, total = evt.routes[evt.host].length; index < total; index++) {
          processIteration(index);
        }
      }
      if (evt && !evt.match) {
        evt.match = true;
        if (evt.wildcardSet && evt.routes[evt.wildcardSet[evt.host]] &&
          evt.routes[evt.wildcardSet[evt.host]][evt.wildcardSet[index]]) {
          evt.currentRoute = evt.routes[evt.wildcardSet[evt.host]][evt.wildcardSet[index]];
        } else if (evt && evt.routes && evt.routes[evt.host] &&
          evt.routes[evt.host][0] && evt.routes[evt.host][0].app) {
          evt.currentRoute = {app: evt.routes[evt.host][0].app};
        }
        if (evt.host === '*' && evt.currentRoute) evt.currentRoute.matchType = 'wildcard';
        else if (evt.currentRoute) evt.currentRoute.matchType = 'direct';
      }
      observer.next(evt);
    }, source);
  };

  /**
   * (Internal > Stream Methods [6]) Check Overrides & Match
   *
   * @private
   * @memberof Server.Modules.AppEngine
   * @function pipelines.search.checkAndMatch
   * @ignore
   * @param {observable} source - The Source Observable
   * @return {observable} destination - The Destination Observable
   *
   * @description
   * Tbc...
   *
   * @example
   * Tbc...
   */
  pipelines.search.checkAndMatch = function AppEngineSPLCheckAndMatch(source) {
    // noinspection JSUnresolvedFunction
    return core.lib.rxOperator(function AppEngineSPLCheckAndMatchOp(observer, evt) {
      if (!evt.results) evt.results = [];
      if (evt.override) evt.currentRoute = evt.override;
      if (evt.match && evt.currentRoute) evt.results.push({match: evt.currentRoute, param: evt.param});
      if (evt.results.length !== 1) {
        const intermediateResults = [];
        for (let i = 0; i < evt.results.length; i++) {
          if (evt.results[i].match.matchType === 'direct') intermediateResults.push(evt.results[i]);
        }
        if (intermediateResults && intermediateResults.length === 1) {
          evt.results = intermediateResults;
          evt.result = evt.results[0];
        } else evt.result = false;
      } else evt.result = evt.results[0];
      o.log('debug',
          'AppEngine > [6] Overrides and matches have been checked',
          {module: mod.name}, 'APPENGINE_OVERRIDES_CHECKED_AND_MATCHED');
      observer.next(evt);
    }, source);
  };
}();