UNPKG

orator

Version:

Unopinionated API http server abstraction - REST or IPC

467 lines (425 loc) 16 kB
/** * Orator Service Abstraction * * @license MIT * * @author Steven Velozo <steven@velozo.com> * @module Orator Service */ const libFableServiceProviderBase = require('fable-serviceproviderbase'); const libDefaultOratorServiceServer = require('./Orator-Default-ServiceServer.js'); // orator-static-server is lazy-loaded so that browser bundles using orator's // IPC mode don't pay the cost of (or trip over) its server-only deps. Only // setMimeHeader / addStaticRoute / addStaticRouteWithFallbacks need it, and // none of those are reachable from IPC-only consumers. let libOratorStaticServer = null; const getOratorStaticServer = () => { if (!libOratorStaticServer) { libOratorStaticServer = require('orator-static-server'); } return libOratorStaticServer; }; const defaultOratorConfiguration = require('./Orator-Default-Configuration.js'); /** * @class Orator * @extends libFableServiceProviderBase * * Represents the Orator service provider. * * @param {Object} pFable - The Fable instance. * @param {Object} pOptions - The options for the Orator service. * @param {string} pServiceHash - The hash of the service. */ class Orator extends libFableServiceProviderBase { constructor(pFable, pOptions, pServiceHash) { super(pFable, pOptions, pServiceHash); this.serviceType = 'Orator'; // Create the empty, important logic containers this.serviceServer = false; this.serviceServerProvider = false; // Now check to see that the ServicePort is set (this used to be APIServerPort) if (!this.options.hasOwnProperty('ServicePort')) { if (this.fable.settings.hasOwnProperty('APIServerPort')) { // Automatically migrate the legacy APIServerPort to ServicePort this.options.ServicePort = this.fable.settings.APIServerPort; } else { // Default to whatever the ... default is! this.options.ServicePort = defaultOratorConfiguration.ServicePort; } } // Now check to see that the Product name is set if (!this.options.hasOwnProperty('Product')) { this.options.Product = defaultOratorConfiguration.Product; } } /** * Lifecycle event that executes before initializing the service. For overloading. */ onBeforeInitialize() { if (this.fable.settings.LogNoisiness > 3) { this.log.trace(`Orator [${this.UUID}]::[${this.Hash}] ${this.options.Product} onBeforeInitialize:`); } } /** * Lifecycle event that executes before initializing the service. For overloading. * * @param {Function} fNext - The callback function to be called after the actions are executed. */ onBeforeInitializeAsync(fNext) { this.onBeforeInitialize(); // Check to see if there is a service server active; if not instantiate one (and use IPC if none is registered with Fable as the default provider) if (!this.serviceServer) { // If the developer hasn't set this to a service provider class of their own choosing, // TODO: Give the developer a chance to set a service provider instantiation address of their own choosing. // use the built-in network-less one. if (!this.fable.OratorServiceServer) { // If there isn't a default Service Server setup, create one. let tmpServiceServerOptions = (typeof(this.options.ServiceServerOptions) == 'undefined') ? {} : this.options.ServiceServerOptions; if (!this.fable.serviceManager.servicesMap.hasOwnProperty('OratorServiceServer')) { // Only register IPC if there isn't one yet. this.fable.serviceManager.addServiceType('OratorServiceServer', libDefaultOratorServiceServer); } this.fable.serviceManager.instantiateServiceProvider('OratorServiceServer', tmpServiceServerOptions, 'OratorServiceServer-AutoInit'); } this.serviceServer = this.fable.OratorServiceServer; // For legacy reasons, we also will provide this under the "webServer" variable. this.webServer = this.serviceServer; } else { this.log.warn(`Orator attempting to initialize a service server after initialization has already completed.`) } fNext(); } onInitialize() { if (this.fable.settings.LogNoisiness > 3) { this.log.trace(`Orator [${this.UUID}]::[${this.Hash}] ${this.options.Product} onInitialize:`); } } /** * Lifecycle event that executes at the moment of initializing the service. For overloading. * * @param {Function} fNext - The callback function to be executed after initialization. */ onInitializeAsync(fNext) { this.onInitialize(); return fNext(); } /** * Lifecycle event that executes after initializing the service. For overloading. */ onAfterInitialize() { if (this.fable.settings.LogNoisiness > 3) { this.log.trace(`Orator [${this.UUID}]::[${this.Hash}] ${this.options.Product} onAfterInitialize:`); } } /** * Lifecycle event that executes after initializing the service. For overloading. * @param {Function} fNext - The callback function to be called after executing onAfterInitialize. * @returns {Promise} - A promise that resolves when the callback function is called. */ onAfterInitializeAsync(fNext) { this.onAfterInitialize(); return fNext(); } /** * Initializes the Orator instance. * * @param {Function} fCallback - The callback function to be executed after initialization. */ initialize(fCallback) { // I hate this -- as long as we want to be "mostly" backwards compatible it needs to do it though let tmpCallback = (typeof(fCallback) === 'function') ? fCallback : () => {}; if (!this.initializeTimestamp) { let tmpAnticipate = this.fable.serviceManager.instantiateServiceProviderWithoutRegistration('Anticipate'); if (this.fable.LogNoisiness > 3) { this.log.trace(`Orator [${this.UUID}]::[${this.Hash}] ${this.options.Product} beginning initialization steps...`); } tmpAnticipate.anticipate(this.onBeforeInitializeAsync.bind(this)); tmpAnticipate.anticipate(this.onInitializeAsync.bind(this)); tmpAnticipate.anticipate(this.onAfterInitializeAsync.bind(this)); tmpAnticipate.wait( (pError) => { this.initializeTimestamp = this.fable.log.getTimeStamp(); if (this.fable.LogNoisiness > 2) { this.log.trace(`Orator [${this.UUID}]::[${this.Hash}] ${this.options.Product} initialization steps complete.`); } return tmpCallback(pError); }); } else { this.log.warn(`Orator [${this.UUID}]::[${this.Hash}] ${this.options.Product} async initialize called but initialization is already completed. Aborting.`); // TODO: Should this be returning an error? return tmpCallback(); } } /** * Lifecycle event that executes before starting the service. For overloading. * * @param {Function} fNext - The function to be executed before starting the service. * @returns {Promise} A promise that resolves when the function is completed. */ onBeforeStartService(fNext) { return fNext(); } /** * Lifecycle event that executes when the service starts. For overloading. * * @param {Function} fNext - The callback function to be called after the service starts. * @returns {Promise} A promise that resolves when the service starts successfully, or rejects with an error. */ onStartService(fNext) { this.onAfterInitialize(); return this.serviceServer.listen ( this.options.ServicePort, (pError) => { this.log.info(`${this.serviceServer.Name} listening on port ${this.options.ServicePort}`); return fNext(pError); } ); } /** * Lifecycle event that executes after starting the service. For overloading. * * @param {Function} fNext - The callback function to be executed after the service starts. * @returns {Promise} - A promise that resolves when the callback function is completed. */ onAfterStartService(fNext) { return fNext(); } /** * Starts the service. * * @param {Function} fNext - The callback function to be executed after the service has started. */ startService(fNext) { var tmpNext = (typeof(fNext) === 'function') ? fNext : ()=>{}; let tmpAnticipate = this.fable.serviceManager.instantiateServiceProviderWithoutRegistration('Anticipate'); if (this.fable.LogNoisiness > 3) { this.log.trace(`Orator [${this.UUID}]::[${this.Hash}] ${this.options.Product} beginning startService steps...`); } // Auto initialize if there is no serviceServer if (!this.serviceServer) { tmpAnticipate.anticipate(this.initialize.bind(this)); } tmpAnticipate.anticipate(this.onBeforeStartService.bind(this)); tmpAnticipate.anticipate(this.onStartService.bind(this)); tmpAnticipate.anticipate(this.onAfterStartService.bind(this)); tmpAnticipate.wait( (pError) => { this.startServiceTimestamp = this.fable.log.getTimeStamp(); if (this.fable.LogNoisiness > 2) { this.log.trace(`Orator [${this.UUID}]::[${this.Hash}] ${this.options.Product} startService steps complete.`); } return tmpNext(pError); }); } /** * Stops the service server. * * @param {Function} fCallback - The callback function to be executed after the service server is stopped. * @returns {void} */ stopService(fCallback) { var tmpCallback = (typeof(fCallback) === 'function') ? fCallback : ()=>{}; if (!this.serviceServer) { let tmpMessage = `Orator attempting to stop a service server but the service server has not been intialized yet.`; this.log.warn(tmpMessage); return tmpCallback(tmpMessage); } if (!this.serviceServer.Active) { let tmpMessage = `Orator attempting to stop a service server but the service server is not actively running.`; this.log.warn(tmpMessage); return tmpCallback(tmpMessage); } return this.serviceServer.close(tmpCallback); } /** * Programmatically invokes a method on the service server. * * @param {string} pMethod - The method to invoke. * @param {string} pRoute - The route to invoke. * @param {any} pData - The data to send with the invocation. * @param {Function} fCallback - The callback function to execute after the invocation. * @returns {any} - The result of the invocation. */ invoke(pMethod, pRoute, pData, fCallback) { //this.log.trace(`Orator [${this.UUID}]::[${this.Hash}] ${this.options.Product} invoking ${pMethod} ${pRoute}`); return this.serviceServer.invoke(pMethod, pRoute, pData, fCallback); } /* * Legacy Orator / Backwards Compatibility Functions *************************************************************************/ /** * Starts the web server. * * @param {Function} fNext - The callback function to be executed after the web server starts. * @returns {Promise} A promise that resolves when the web server has started. */ startWebServer(fNext) { return this.startService(fNext); } /** * Stops the web server. * * @param {Function} fNext - The callback function to be called after the web server is stopped. * @returns {Promise} A promise that resolves when the web server is stopped. */ stopWebServer(fNext) { return this.stopService(fNext); } /** * Retrieves the web server instance. * * @returns {WebServer} The web server instance. */ getWebServer() { // The old behavior was to lazily construct the service the first time // this accessor function is called. if (!this.serviceServer) { this.initialize(); } return this.serviceServer; } /************************************************************************* * End of Legacy Orator Functions */ /* * Backwards Compatibility: setMimeHeader and oldLibMime * * These used to live directly on Orator but now live on OratorStaticServer. * These thin wrappers delegate to the OratorStaticServer instance so that * any code calling orator.setMimeHeader() or reading orator.oldLibMime * continues to work without changes. *************************************************************************/ /** * @deprecated Use fable.OratorStaticServer.oldLibMime instead. */ get oldLibMime() { if (this.fable.OratorStaticServer) { return this.fable.OratorStaticServer.oldLibMime; } return false; } /** * @deprecated Use fable.OratorStaticServer.setMimeHeader() instead. */ setMimeHeader(pFileName, pResponse) { if (!this.fable.OratorStaticServer) { // Force auto-registration so the method is available if (!this.fable.serviceManager.servicesMap.hasOwnProperty('OratorStaticServer')) { this.fable.serviceManager.addServiceType('OratorStaticServer', getOratorStaticServer()); } this.fable.serviceManager.instantiateServiceProvider('OratorStaticServer', {}, 'OratorStaticServer-AutoInit'); } return this.fable.OratorStaticServer.setMimeHeader(pFileName, pResponse); } /** * Serve a static folder, optionally with magic subdomain masking. * Delegates to the OratorStaticServer service provider. * * @param {string} pFilePath The path on disk that we are serving files from. * @param {string?} pDefaultFile (optional) The default file served if no specific file is requested. * @param {string?} pRoute (optional) The route matcher that will be used. Defaults to everything. * @param {string?} pRouteStrip (optional) If provided, this prefix will be removed from URL paths before being served. * @param {object?} pParams (optional) Additional parameters to pass to serve-static. * @return {boolean} true if the handler was successfully installed, otherwise false. */ addStaticRoute(pFilePath, pDefaultFile, pRoute, pRouteStrip, pParams) { // Auto-register the OratorStaticServer service type if it hasn't been registered yet if (!this.fable.serviceManager.servicesMap.hasOwnProperty('OratorStaticServer')) { this.fable.serviceManager.addServiceType('OratorStaticServer', getOratorStaticServer()); } // Auto-instantiate a default OratorStaticServer instance if none exists if (!this.fable.OratorStaticServer) { this.fable.serviceManager.instantiateServiceProvider('OratorStaticServer', {}, 'OratorStaticServer-AutoInit'); } return this.fable.OratorStaticServer.addStaticRoute(pFilePath, pDefaultFile, pRoute, pRouteStrip, pParams); } /** * Register a static-file route with a per-file CDN fallback map. * * Behaves like addStaticRoute for every local hit. For any request whose * relative path is a key in pFallbackMap and whose file isn't present * on disk, responds with 302 to the mapped URL instead of a 404. Any * request whose relative path isn't in the map behaves exactly as * addStaticRoute would (local hit → stream, local miss → 404). * * @param {string} pFilePath The path on disk that we are serving files from. * @param {string?} pDefaultFile (optional) The default file served if no specific file is requested. * @param {string?} pRoute (optional) The route matcher that will be used. Defaults to everything. * @param {string?} pRouteStrip (optional) If provided, this prefix will be removed from URL paths before being served. * @param {object?} pParams (optional) Additional parameters to pass to serve-static. * @param {Object<string,string>?} pFallbackMap (optional) Map of relative path (under the route prefix) to absolute URL for CDN fallback. * @return {boolean} true if the handler was successfully installed, otherwise false. */ addStaticRouteWithFallbacks(pFilePath, pDefaultFile, pRoute, pRouteStrip, pParams, pFallbackMap) { if (!this.fable.serviceManager.servicesMap.hasOwnProperty('OratorStaticServer')) { this.fable.serviceManager.addServiceType('OratorStaticServer', getOratorStaticServer()); } if (!this.fable.OratorStaticServer) { this.fable.serviceManager.instantiateServiceProvider('OratorStaticServer', {}, 'OratorStaticServer-AutoInit'); } return this.fable.OratorStaticServer.addStaticRouteWithFallbacks( pFilePath, pDefaultFile, pRoute, pRouteStrip, pParams, pFallbackMap); } } module.exports = Orator; module.exports.ServiceServerBase = require('orator-serviceserver-base'); module.exports.ServiceServerIPC = require('./Orator-ServiceServer-IPC.js');