UNPKG

feature-u

Version:

Feature Based Project Organization for React

954 lines (736 loc) 41.5 kB
import React from 'react'; import isFunction from 'lodash.isfunction'; import isPlainObject from 'lodash.isplainobject'; import verify from '../util/verify'; import {isAspectProperty} from '../extend/createAspect'; import {isFeatureProperty} from './createFeature'; import createFassets from './createFassets'; import {FassetsContext} from './withFassets'; import logf from '../util/logf'; let executionOrder = 1; // running counter of execution order of life-cycle-hooks (unit-test related) /** * Launch an application by assembling the supplied features, driving * the configuration of the frameworks in use _(as orchestrated by the * supplied set of plugable Aspects)_. * * For more information _(with examples)_, please refer to * {{book.guide.detail_launchingApp}}. * * **Please Note** this function uses named parameters. * * @param {Feature[]} features the features that comprise this * application. * * @param {Aspect[]} [aspects] the set of plugable Aspects that extend * **feature-u**, integrating other frameworks to match your specific * run-time stack.<br/><br/> * * When NO Aspects are supplied _(an atypical case)_, only the very * basic **feature-u** characteristics are in effect (like fassets * and life-cycle hooks). * * @param {registerRootAppElmCB} registerRootAppElm the callback hook * that registers the supplied root application element to the specific * React framework used in the app.<br/><br/> * * Because this registration is accomplished by app-specific code, * **feature-u** can operate in any of the react platforms, such as: * {{book.ext.react}} web, {{book.ext.reactNative}}, * {{book.ext.expo}}, etc.<br/><br/> * * Please refer to {{book.guide.detail_reactRegistration}} for more * details and complete examples. * * @param {showStatusCB} [showStatus] an optional callback hook that * communicates a blocking "persistent" status message to the end * user. * * Please refer to {{book.api.showStatusCB}} for more information. * * @return {Fassets} the Fassets object used in * cross-feature-communication. * * @function launchApp */ export default function launchApp({features, aspects=[], registerRootAppElm, showStatus=showStatusFallback, ...unknownArgs}={}) { logf('STARTING: - your application is now starting up'); // reset: running counter of execution order of life-cycle-hooks (unit-test related) executionOrder = 1; // validate launchApp() parameters const check = verify.prefix('launchApp() parameter violation: '); // ... aspects const aspectMap = op.alch.genesis(aspects); // ... features check(features, 'features is required'); check(Array.isArray(features), 'features must be a Feature[] array'); // ... registerRootAppElm check(registerRootAppElm, 'registerRootAppElm is required'); check(isFunction(registerRootAppElm), 'registerRootAppElm must be a function'); // ... showStatus check(showStatus, 'showStatus is required'); check(isFunction(showStatus), 'showStatus must be a function'); // ... unrecognized named parameter const unknownArgKeys = Object.keys(unknownArgs); check(unknownArgKeys.length === 0, `unrecognized named parameter(s): ${unknownArgKeys}`); // ... unrecognized positional parameter check(arguments.length === 1, 'unrecognized positional parameters (only named parameters can be specified)'); // perform "Aspect" property validation op.helper.validateAspectProperties(aspects); // perform "Feature" property validation op.alch.validateFeatureContent(features, aspectMap); // prune to activeFeatures, insuring all feature.names are unique const activeFeatures = op.helper.pruneActiveFeatures(features); // accumulate all feature assets in our Fassets object (used in cross-feature-communication) const fassets = createFassets(activeFeatures); // expand the feature content of any aspect that relies on expandWithFassets() // ... AND perform a delayed validation, once expansion has occurred op.alch.expandFeatureContent(fassets, activeFeatures, aspects); // assemble content of each aspect across all features op.alch.assembleFeatureContent(fassets, activeFeatures, aspects); // assemble resources for each aspect across all other aspects, ONCE ALL aspects have assembled their feature content op.alch.assembleAspectResources(fassets, aspects); // define our rootAppElm via DOM injections from a combination of Aspects/Features // ... also apply Feature.appWillStart() life-cycle hook const rootAppElm = op.helper.defineRootAppElm(fassets, activeFeatures, aspects); // start our app by registering our rootAppElm to the appropriate react framework // NOTE 1: Because this is accomplished by app-specific code, // feature-u can operate in any number of containing react frameworks, // like React Web, React Native, Expo, etc. // NOTE 2: We delay this process (via timeout) making "import fassets" feasible // to UI rendering functions ... such as redux connect(). // This technique allows launchApp() to complete, and the fassets object // to have definition (via application code export) // for use in these UI rendering functions. // HOWEVER: We subsequently discovered that this timeout is // NOT compatible with Expo (or react-native - not sure which) // ERROR: Module AppRegistry is not a registered callable module (calling runApplication) // EVIDENTLY the designated expo mainline cannot run to completion // without first registering a component in some way // ... ex: Expo.registerRootComponent(...); //setTimeout(() => { // remove timeout (see "NOTE 2" above) registerRootAppElm(rootAppElm, fassets); //}, 0); // wrap the showStatus() function to prune duplicate status // ... this happens INTERNALLY when monitoring the next unresolved asynchronous appInit() process const priorStatus = { msg: undefined, err: undefined, }; function showStatusNoDupes(msg, err) { // no-op on duplicate back-to-back status if (msg === priorStatus.msg && err === priorStatus.err) { return; } // retain the status info NOW reported priorStatus.msg = msg; priorStatus.err = err; // pass-through to supplied function showStatus(msg, err); } // gather the set of additional "Aspect Injected" named parameters // to pass into our remaining Application Life Cycle Hooks const additionalHooksParams = op.alch.injectParamsInHooks(fassets, aspects); // apply Feature.appInit() life-cycle hook op.flch.appInit(fassets, activeFeatures, additionalHooksParams, showStatusNoDupes) .then( () => { // >>> once all async processes of feature.appInit() have completed, // continue on with our launchApp() processes // because of the "covert" async nature of launchApp(), // we report any errors via the showStatus() mechanism. try { // apply Feature.appDidStart() life-cycle hook op.flch.appDidStart(fassets, activeFeatures, additionalHooksParams); logf('COMPLETE: Your application has now started'); } catch(err) { const errMsg = 'A problem was encountered in a appDidStart() life-cycle hook'; logf(`INCOMPLETE: Your application did NOT start ... ${errMsg}`, err); showStatusNoDupes(errMsg, err); } }); // expose our new App object (used in feature cross-communication) return fassets; } //*** //*** A secret diagnostic hook attached to the launchApp() function. //*** launchApp.diag = { logf, // allow client to enable feature-u logs via: launchApp.diag.logf.enable(); }; //*** //*** Specification: registerRootAppElmCB //*** /** * The {{book.api.launchApp}} callback hook that registers the * supplied root application element to the specific React framework * used in the app. * * Because this registration is accomplished by app-specific code, * **feature-u** can operate in any of the React platforms, such as: * {{book.ext.react}} web, {{book.ext.reactNative}}, * {{book.ext.expo}}, etc. * * Please refer to {{book.guide.detail_reactRegistration}} for more * details and complete examples. * * @callback registerRootAppElmCB * * @param {reactElm} rootAppElm - the root application element to be * registered. * * @param {Fassets} fassets the Fassets object used in cross-feature-communication * (rarely needed except to allow client to inject their own * FassetsContext.Provider for a null rootAppElm). * * @return void */ //*** //*** Specification: showStatusCB //*** /** * The optional {{book.api.launchApp}} callback hook that communicates * a blocking "persistent" status message to the end user. * * These status messages originate from the blocking that occurs in * the asynchronous processes managed by the {{book.guide.appInitCB}} * life-cycle-hook. * * By design **feature-u** has no ability to manifest messages to the * end user, because this is very app-specific in styling and other * heuristics. By default (when **NO** `showStatus` parameter is * supplied, **feature-u** will simply **console log** these messages. * * A typical manifestation of this callback is to display a running * persistent SplashScreen, seeded with the supplied message. The * SplashScreen should be taken down when NO message is supplied * (i.e. `''`). * * Please refer to {{book.guide.appInitCB}} for more details and * examples. * * @callback showStatusCB * * @param {string} [msg] - the "persistent" message to display. When * NO message is supplied (i.e. `''`), **all** user notifications * should be cleared _(for example, take the SplashScreen down)_. * * @param {Error} [err] - an optional error to communicate to the * user. * * @return void */ //*** //*** Operations Bundle (broken out for testability) //*** /* * A bundle of launchApp() operations (i.e. functions) * - exported internally * - supporting isolated unit testing * * Because we use a bundled container, it provides rudimentary * testing hooks for things like: * - mocking * - monkey patching * - etc. */ export const op = { alch: {}, // aspect-life-cycle-hook ... see definitions (below) flch: {}, // feature-life-cycle-hook ... see definitions (below) helper: {}, // general helpers ... see definitions (below) }; //*-------------------------------------------------------------- //* aspect-life-cycle-hook: genesis(aspects): aspectMap //*-------------------------------------------------------------- op.alch.genesis = function(aspects) { // maintain: running counter of execution order of life-cycle-hooks (unit-test related) op.alch.genesis.executionOrder = executionOrder++; const check = verify.prefix('launchApp() parameter violation: '); check(Array.isArray(aspects), 'aspects (when supplied) must be an Aspect[] array'); logf('the following Aspects are in effect: ' + aspects.map( (aspect) => `\n Aspect.name:${aspect.name}` )); // log summary const hookCount = aspects.reduce( (count, aspect) => aspect.genesis ? count+1 : count, 0); const hookSummary = aspects.map( (aspect) => `\n Aspect.name:${aspect.name}${aspect.genesis ? ' <-- defines: genesis()' : ''}` ); logf(`aspect-life-cycle-hook ... PROCESSING: Aspect.genesis() ... ${hookCount} hooks:${hookSummary}`); // our convenient hash of aspects // ... keyed by aspectName // aspectMap[aspectName]: aspect const aspectMap = aspects.reduce( (accum, aspect) => { // the set of aspects in use MUST have unique names if (accum[aspect.name]) { check(false, `supplied aspects contain a NON-unique name: '${aspect.name}'`); } // allow each aspect to perform Aspect related initialization and validation if (aspect.genesis) { logf(`aspect-life-cycle-hook ... Aspect.name:${aspect.name} ... invoking it's defined Aspect.genesis()`); const errMsg = aspect.genesis(); check(!errMsg, errMsg); // non-null is considered a validation error } // maintain our aspect hash accum[aspect.name] = aspect; return accum; }, {}); return aspectMap; }; //*--------------------------------------------------- //* helper: validateAspectProperties(aspects): void //*--------------------------------------------------- op.helper.validateAspectProperties = function(aspects) { // perform "Aspect" property validation // NOTE 1: This is done here rather than createAspect(), because // all Aspects need to be expanded (i.e. imported) for any extendAspectProperty() to be executed) // ... this will have been done by the time launchApp() is executed!! // NOTE 2: The original source of this error is in createAspect(), // so we prefix any errors as such! const check = verify.prefix('createAspect() parameter violation: '); aspects.forEach( aspect => { // for each aspect for (const propName in aspect) { // iterate over the aspects props // handle unrecognized Aspect.property // ... NOTE: Aspect extended properties have already been added to this isAspectProperty() list // via extendAspectProperty() // executed early within the extending Aspect (i.e. in genesis()) // SO it is in the list at this time!! check(isAspectProperty(propName), `Aspect.name: '${aspect.name}' contains unrecognized property: ${propName} ... no Aspect is registered to handle this!`); } }); }; //*----------------------------------------------------------------------------- //* aspect-life-cycle-hook: validateFeatureContent(features, aspectMap): void //*----------------------------------------------------------------------------- op.alch.validateFeatureContent = function(features, aspectMap) { // NOTE: nothing to logf() here BECAUSE entire result is potential EXCEPTIONS // maintain: running counter of execution order of life-cycle-hooks (unit-test related) op.alch.validateFeatureContent.executionOrder = executionOrder++; // log summary const aspects = []; // convert aspectMap to array ... do this RATHER than change API JUST to accommodate logging for (const propKey in aspectMap) { aspects.push(aspectMap[propKey]); } const hookCount = aspects.reduce( (count, aspect) => aspect.validateFeatureContent ? count+1 : count, 0); const hookSummary = aspects.map( (aspect) => `\n Aspect.name:${aspect.name}${aspect.validateFeatureContent ? ' <-- defines: validateFeatureContent()' : ''}` ); logf(`aspect-life-cycle-hook ... PROCESSING: Aspect.validateFeatureContent() ... ${hookCount} hooks:${hookSummary}`); // perform "Feature" property validation // NOTE 1: This is done here rather than createFeature(), because it's the aspect's // responsibility, and this is the spot where we have aspect context. // NOTE 2: The original source of this error is in createFeature(), // so we prefix any errors as such! const check = verify.prefix('createFeature() parameter violation: '); features.forEach( feature => { // for each feature for (const propName in feature) { // iterate over the features props // we only validate non built-in keywords // ... built-ins are validated by createFeature() // ... these extra props will be processed by an Aspect // ... an error condition if if no Aspect is registered to handle it // ... NOTE: Aspect extended properties have already been added to this isFeatureProperty() list // via extendFeatureProperty() // executed early within the extending Aspect (i.e. in genesis()) // SO it is in the list at this time!! if (!isFeatureProperty(propName)) { // locate the aspect that will process this item const aspect = aspectMap[propName]; // handle unrecognized aspect check(aspect, `Feature.name: '${feature.name}' contains unrecognized property: ${propName} ... no Aspect is registered to handle this!`); // delay validation when expansion is needed // ... is accomplished in subsequent step (after expansion has occurred) // ... this means that validation logic in aspect does NOT have to worry about .expandWithFassets if (!feature[propName].expandWithFassets) { // allow the aspect to validate it's content // ... ex: a reducer MUST be a function (or expandWithFassets) and it must have a shape! logf(`aspect-life-cycle-hook ... Aspect.name:${aspect.name} ... invoking it's required Aspect.validateFeatureContent() on Feature.name:${feature.name}'s Feature.${aspect.name}`); const errMsg = aspect.validateFeatureContent(feature); // validate self's aspect on supplied feature (which is known to contain this aspect) check(!errMsg, errMsg); // non-null is considered a validation error } } } }); }; //*---------------------------------------------------------------- //* helper: pruneActiveFeatures(features): activeFeatures //*---------------------------------------------------------------- op.helper.pruneActiveFeatures = function(features) { const check = verify.prefix('launchApp() parameter violation: '); // prune to activeFeatures, insuring all feature.names are unique const allFeatureNames = {}; const activeFeatures = features.filter( feature => { check(!allFeatureNames[feature.name], `feature.name: '${feature.name}' is NOT unique`); allFeatureNames[feature.name] = true; return feature.enabled; }); logf('the following Features were supplied: ' + features.map( (feature) => `\n Feature.name:${feature.name}${feature.enabled ? '' : ' <<< NOT ACTIVE'}`)); logf('the following Features are in effect (i.e. active): ' + activeFeatures.map( (feature) => `\n Feature.name:${feature.name}` )); return activeFeatures; }; //*-------------------------------------------------------------------------------------- //* aspect-life-cycle-hook: expandFeatureContent(fassets, activeFeatures, aspects): void //*-------------------------------------------------------------------------------------- op.alch.expandFeatureContent = function(fassets, activeFeatures, aspects) { // maintain: running counter of execution order of life-cycle-hooks (unit-test related) op.alch.expandFeatureContent.executionOrder = executionOrder++; // log summary const hookCount = aspects.reduce( (count, aspect) => aspect.expandFeatureContent ? count+1 : count, 0); const hookSummary = aspects.map( (aspect) => `\n Aspect.name:${aspect.name}${aspect.expandFeatureContent ? ' <-- defines: expandFeatureContent()' : ''}` ); logf(`resolving expandWithFassets() ... either by DEFAULT-PROCESS -OR- aspect-life-cycle-hook PROCESSING: Aspect.expandFeatureContent() ... ${hookCount} hooks:${hookSummary}`); // expand the feature content of any aspect that relies on expandWithFassets() // ... AND perform a delayed validation, once expansion has occurred // NOTE: The original source of this error is in createFeature(), // so we prefix any errors as such! const check = verify.prefix('createFeature() parameter violation: '); aspects.forEach( aspect => { activeFeatures.forEach( feature => { if (feature[aspect.name] && feature[aspect.name].expandWithFassets) { let errMsg = null; // perform the expansion if (aspect.expandFeatureContent) { // aspect wishes to do this // ... a simple process, BUT provides the hook to do more (ex: reducer transfer of slice) logf(`resolving expandWithFassets() [by aspect-life-cycle-hook Aspect.name:${aspect.name}'s Aspect.expandFeatureContent()] ON Feature.name:${feature.name}'s Feature.${aspect.name} AspectContent`); errMsg = aspect.expandFeatureContent(fassets, feature); // ... specialized validation, over-and-above the validateFeatureContent() hook check(!errMsg, errMsg); // truthy is considered a validation error } else { logf(`resolving expandWithFassets() [by DEFAULT-PROCESS] ON Feature.name:${feature.name}'s Feature.${aspect.name} AspectContent`); // default implementation (when not done by the aspect) feature[aspect.name] = feature[aspect.name](fassets); } // perform our delayed validation logf(`aspect-life-cycle-hook ... Aspect.name:${aspect.name} ... invoking it's required Aspect.validateFeatureContent() on Feature.name:${feature.name}'s Feature.${aspect.name} ... DELAYED from expandWithFassets()`); errMsg = aspect.validateFeatureContent(feature); // validate self's aspect on supplied feature (which is known to contain this aspect) check(!errMsg, errMsg); // truthy is considered a validation error } }); }); }; //*---------------------------------------------------------------------------------------- //* aspect-life-cycle-hook: assembleFeatureContent(fassets, activeFeatures, aspects): void //*---------------------------------------------------------------------------------------- op.alch.assembleFeatureContent = function(fassets, activeFeatures, aspects) { // maintain: running counter of execution order of life-cycle-hooks (unit-test related) op.alch.assembleFeatureContent.executionOrder = executionOrder++; // log summary const hookCount = aspects.reduce( (count, aspect) => aspect.assembleFeatureContent ? count+1 : count, 0); const hookSummary = aspects.map( (aspect) => `\n Aspect.name:${aspect.name}${aspect.assembleFeatureContent ? ' <-- defines: assembleFeatureContent()' : ''}` ); logf(`aspect-life-cycle-hook ... PROCESSING: Aspect.assembleFeatureContent() ... ${hookCount} hooks:${hookSummary}`); // assemble content of each aspect across all features // ... retaining needed state for subsequent ops aspects.forEach( aspect => { logf(`aspect-life-cycle-hook ... Aspect.name:${aspect.name} ... invoking it's required Aspect.assembleFeatureContent()`); aspect.assembleFeatureContent(fassets, activeFeatures); }); }; //*------------------------------------------------------------------------- //* aspect-life-cycle-hook: assembleAspectResources(fassets, aspects): void //*------------------------------------------------------------------------- op.alch.assembleAspectResources = function(fassets, aspects) { // maintain: running counter of execution order of life-cycle-hooks (unit-test related) op.alch.assembleAspectResources.executionOrder = executionOrder++; // log summary const hookCount = aspects.reduce( (count, aspect) => aspect.assembleAspectResources ? count+1 : count, 0); const hookSummary = aspects.map( (aspect) => `\n Aspect.name:${aspect.name}${aspect.assembleAspectResources ? ' <-- defines: assembleAspectResources()' : ''}` ); logf(`aspect-life-cycle-hook ... PROCESSING: Aspect.assembleAspectResources() ... ${hookCount} hooks:${hookSummary}`); // assemble resources for each aspect across all other aspects, ONCE ALL aspects have assembled their feature content // ... retaining needed state for subsequent ops aspects.forEach( aspect => { if (aspect.assembleAspectResources) { logf(`aspect-life-cycle-hook ... Aspect.name:${aspect.name} ... invoking it's defined Aspect.assembleAspectResources()`); aspect.assembleAspectResources(fassets, aspects); } }); }; //*------------------------------------------------------------------------ //* helper: defineRootAppElm(fassets, activeFeatures, aspects): rootAppElm //*------------------------------------------------------------------------ op.helper.defineRootAppElm = function(fassets, activeFeatures, aspects) { // define our curRootAppElm via DOM injections from a combination of Aspects/Features let rootAppElm = null; // we start with nothing logf('defining-rootAppElm ... starting process, rootAppElm: null'); // FIRST: DOM injection via Aspect.initialRootAppElm(fassets, curRootAppElm) rootAppElm = op.alch.initialRootAppElm(fassets, aspects, rootAppElm); // SECOND: DOM injection via Feature.appWillStart() life-cycle hook rootAppElm = op.flch.appWillStart(fassets, activeFeatures, rootAppElm); // THIRD: DOM injection via Aspect.injectRootAppElm() rootAppElm = op.alch.injectRootAppElm(fassets, aspects, rootAppElm); // FOURTH: inject our <FassetsContext.Provider> in support of withFassets() HoC // NOTE: We conditionally do this if a rootAppElm has been defined. // Otherwise, the App is responsible for this in the registerRootAppElm() hook. if (rootAppElm) { rootAppElm = <FassetsContext.Provider value={fassets}>{rootAppElm}</FassetsContext.Provider>; } // NOTE: We do NOT validate rootAppElm to insure it is non-null! // - at first glance it would appear that a null rootAppElm would render NOTHING // - HOWEVER, ULTIMATELY the app code (found in the registerRootAppElm() hook) // can display whatever it wants ... a given app may have chosen to inject it's own rootAppElm logf('defining-rootAppElm ... complete, rootAppElm: ', logf.elm2html(rootAppElm)); return rootAppElm; }; //*---------------------------------------------------------------------------------------- //* aspect-life-cycle-hook: initialRootAppElm(fassets, aspects, curRootAppElm): rootAppElm //*---------------------------------------------------------------------------------------- op.alch.initialRootAppElm = function(fassets, aspects, curRootAppElm) { // maintain: running counter of execution order of life-cycle-hooks (unit-test related) op.alch.initialRootAppElm.executionOrder = executionOrder++; // log summary const hookCount = aspects.reduce( (count, aspect) => aspect.initialRootAppElm ? count+1 : count, 0); const hookSummary = aspects.map( (aspect) => `\n Aspect.name:${aspect.name}${aspect.initialRootAppElm ? ' <-- defines: initialRootAppElm()' : ''}` ); logf(`aspect-life-cycle-hook ... PROCESSING: Aspect.initialRootAppElm() ... ${hookCount} hooks:${hookSummary}`); // DOM injection via Aspect.initialRootAppElm(fassets, curRootAppElm) return aspects.reduce( (curRootAppElm, aspect) => { if (aspect.initialRootAppElm) { logf(`aspect-life-cycle-hook ... Aspect.name:${aspect.name} ... invoking it's defined Aspect.initialRootAppElm()`); const rootAppElm = aspect.initialRootAppElm(fassets, curRootAppElm); if (rootAppElm !== curRootAppElm) { logf(`defining-rootAppElm ... Aspect.name:${aspect.name}'s Aspect.initialRootAppElm() CHANGED rootAppElm: `, logf.elm2html(rootAppElm)); } return rootAppElm; } else { return curRootAppElm; } }, curRootAppElm ); }; //*------------------------------------------------------------------------------------------- //* feature-life-cycle-hook: appWillStart(fassets, activeFeatures, curRootAppElm): rootAppElm //*------------------------------------------------------------------------------------------- op.flch.appWillStart = function(fassets, activeFeatures, curRootAppElm) { // maintain: running counter of execution order of life-cycle-hooks (unit-test related) op.flch.appWillStart.executionOrder = executionOrder++; // log summary const hookCount = activeFeatures.reduce( (count, feature) => feature.appWillStart ? count+1 : count, 0); const hookSummary = activeFeatures.map( (feature) => `\n Feature.name:${feature.name}${feature.appWillStart ? ' <-- defines: appWillStart()' : ''}` ); logf(`feature-life-cycle-hook ... PROCESSING: Feature.appWillStart() ... ${hookCount} hooks:${hookSummary}`); // DOM injection via Feature.appWillStart() life-cycle hook // - can perform ANY initialization // - AND supplement our top-level content (using a non-null return) // ... wedged between the two Aspect DOM injections (in support of various Aspect needs) return activeFeatures.reduce( (curRootAppElm, feature) => { if (feature.appWillStart) { logf(`feature-life-cycle-hook ... Feature.name:${feature.name} ... invoking it's defined Feature.appWillStart()`); const rootAppElm = feature.appWillStart({fassets, curRootAppElm}) || curRootAppElm; if (rootAppElm !== curRootAppElm) { logf(`defining-rootAppElm ... Feature.name:${feature.name}'s Feature.appWillStart() CHANGED rootAppElm: `, logf.elm2html(rootAppElm)); } return rootAppElm; } else { return curRootAppElm; } }, curRootAppElm ); }; //*--------------------------------------------------------------------------------------- //* aspect-life-cycle-hook: injectRootAppElm(fassets, aspects, curRootAppElm): rootAppElm //*--------------------------------------------------------------------------------------- op.alch.injectRootAppElm = function(fassets, aspects, curRootAppElm) { // maintain: running counter of execution order of life-cycle-hooks (unit-test related) op.alch.injectRootAppElm.executionOrder = executionOrder++; // log summary const hookCount = aspects.reduce( (count, aspect) => aspect.injectRootAppElm ? count+1 : count, 0); const hookSummary = aspects.map( (aspect) => `\n Aspect.name:${aspect.name}${aspect.injectRootAppElm ? ' <-- defines: injectRootAppElm()' : ''}` ); logf(`aspect-life-cycle-hook ... PROCESSING: Aspect.injectRootAppElm() ... ${hookCount} hooks:${hookSummary}`); // DOM injection via Aspect.injectRootAppElm() return aspects.reduce( (curRootAppElm, aspect) => { if (aspect.injectRootAppElm) { logf(`aspect-life-cycle-hook ... Aspect.name:${aspect.name} ... invoking it's defined Aspect.injectRootAppElm()`); const rootAppElm = aspect.injectRootAppElm(fassets, curRootAppElm); if (rootAppElm !== curRootAppElm) { logf(`defining-rootAppElm ... Aspect.name:${aspect.name}'s Aspect.injectRootAppElm() CHANGED rootAppElm: `, logf.elm2html(rootAppElm)); } return rootAppElm; } else { return curRootAppElm; } }, curRootAppElm ); }; //*--------------------------------------------------------------------------------------- //* aspect-life-cycle-hook: injectParamsInHooks(fassets, aspects): namedParams //*--------------------------------------------------------------------------------------- op.alch.injectParamsInHooks = function (fassets, aspects) { // maintain: running counter of execution order of life-cycle-hooks (unit-test related) op.alch.injectParamsInHooks.executionOrder = executionOrder++; // gather the set of additional "Aspect Injected" named parameters // to pass into our remaining Application Life Cycle Hooks const allNamedParams = aspects.reduce( (accumNamedParams, aspect) => { // when this aspect provides the hook, gather it's injected params if (aspect.injectParamsInHooks) { // invoke the aspect's hook // + injectParamsInHooks(fassets): namedParams logf(`aspect-life-cycle-hook ... Aspect.name:${aspect.name} ... invoking it's defined Aspect.injectParamsInHooks()`); const aspectNamedParams = aspect.injectParamsInHooks(fassets); // verify the return value const check = verify.prefix(`Aspect.name:${aspect.name} injectParamsInHooks() return violation: `); // ... required check(aspectNamedParams, 'nothing was returned ... expecting namedParams (a plain object) ... use empty object {} for nothing'); // ... expecting namedParams (a plain object) check(isPlainObject(aspectNamedParams), `expecting namedParams (a plain object) ... NOT: ${aspectNamedParams}`); // ... insure NO name clashes with other aspect injections const aspectNamedParamKeys = Object.keys(aspectNamedParams); const accumNamedParamKeys = Object.keys(accumNamedParams); const nameClashes = accumNamedParamKeys.filter( (entry) => aspectNamedParamKeys.includes(entry) ); check(nameClashes.length === 0, `the following parameter names clashed with other aspects: ${nameClashes}`); // ... insure NO reserved words are used const reservedNamedParamKeys = ['showStatus', 'fassets']; // from app-life-cycle-hooks: appInit(), and appDidStart() const reservedNameClashes = reservedNamedParamKeys.filter( (entry) => aspectNamedParamKeys.includes(entry) ); check(reservedNameClashes.length === 0, `the following parameter names are reserved by feature-u and cannot be used: ${reservedNameClashes}`); // accumulate this aspect's namedParams accumNamedParams = {...accumNamedParams, ...aspectNamedParams}; } // keep accumulating from other aspects return accumNamedParams; }, {} ); // that's all folks return allNamedParams; }; //*----------------------------------------------------------------------------------------- //* feature-life-cycle-hook: appInit(fassets, activeFeatures, additionalHooksParams, showStatus): promise //*----------------------------------------------------------------------------------------- op.flch.appInit = function(fassets, activeFeatures, additionalHooksParams, showStatus) { // wrap entire process // ... will resolve when ALL feature.appInit() have completed! return new Promise( (resolve, reject) => { // maintain: running counter of execution order of life-cycle-hooks (unit-test related) op.flch.appInit.executionOrder = executionOrder++; // log summary const hookCount = activeFeatures.reduce( (count, feature) => feature.appInit ? count+1 : count, 0); const hookSummary = activeFeatures.map( (feature) => `\n Feature.name:${feature.name}${feature.appInit ? ' <-- defines: appInit()' : ''}` ); logf(`feature-life-cycle-hook ... PROCESSING: Feature.appInit() ... ${hookCount} hooks:${hookSummary}`); // invoke Feature.appInit() life-cycle hooks // ... accomplished in AsyncInit class const asyncInits = activeFeatures.reduce( (accum, feature) => { if (feature.appInit) { accum.push( new AsyncInit(feature, fassets, additionalHooksParams, showStatus, monitorNextAsyncInit) ); } return accum; }, []); // monitor the FIRST incomplete AsyncInit monitorNextAsyncInit(); // actively monitor the next incomplete AsyncInit function monitorNextAsyncInit() { // monitor the next incomplete AsyncInit const nextIncompleteAsyncInit = asyncInits.find( (asyncInit) => !asyncInit.complete ); if (nextIncompleteAsyncInit) { nextIncompleteAsyncInit.monitor(); } // otherwise (if there are NO MORE), we are done else { // clear status notification to user showStatus(''); // ... use '', just in case we are invoking app-supplied showStatus() // ALL feature.appInit() have completed! // ... resolve our promise return resolve('ALL feature.appInit() have completed!'); } } }); // end of ... promise }; // the default showStatus callback ... merely logs status messages function showStatusFallback(msg='', err=null) { const postMsg = `STATUS CHANGE (from appInit() life cycle hook): '${msg}'`; if (err) { logf.force(`${postMsg} WITH ERROR: ${err}`, err); } else { logf.force(postMsg); } } // our helper class to launch/monitor async processes in feature.appInit() class AsyncInit { // class constructor constructor(feature, // feature KNOWN TO HAVE appInit() hook fassets, additionalHooksParams, showStatusApp, // the app-specific showStatus() callback function monitorNextAsyncInit) { // carve out instance fields here this.feature = feature; // the feature object KNOWN to have appInit() hook this.showStatusApp = showStatusApp; // the app-specific showStatus() callback function this.monitored = false; // is process being monitored by our external tracker this.complete = false; // is process complete (either with or without error) // the process's current status message (defaulted to feature name) this.statusMsg = `initializing feature: ${feature.name}`; this.err = null; // the error (if any) from this AsyncInit process // bind our showStatus() instance method, so it can be used as a direct function this.showStatus = this.showStatus.bind(this); // invoke the feature.appInit() hook logf(`feature-life-cycle-hook ... Feature.name:${feature.name} ... invoking it's defined Feature.appInit()`); let optionalPromise = null; try { optionalPromise = feature.appInit({showStatus: this.showStatus, fassets, ...additionalHooksParams}); } catch(err) { this.err = err; } // monitor promise completion (when returned) if (optionalPromise && optionalPromise.then) { // a promise was returned optionalPromise.then( () => { // feature.appInit() finished successfully logf(`AsyncInit: finished async process for feature: ${this.feature.name} - '${this.statusMsg}'`); // consider this a completion (with lack of err) this.complete = true; // now monitor the next AsyncInit process (if any) monitorNextAsyncInit(); }) .catch( (err) => { // feature.appInit() finished WITH error logf(`AsyncInit: finished async process for feature: ${this.feature.name} - '${this.statusMsg}' WITH ERROR: ${err}`, err); // communicate error condition to user // NOTE: this retains err in self (in a "sticky" way) this.showStatus(this.statusMsg, err); }); } // when NO promise was returned, we consider it complete "from the start" // ... unless it errored out else { if (!this.err) { this.complete = true; } } } // mark this AsyncInit as now being actively monitored (by our external tracking process) monitor() { // mark self as being actively monitored this.monitored = true; // communicate to user what we are waiting for // ... and error condition (if previously errored out) this.showStatus(this.statusMsg, this.err); } // our showStatus(), used by the feature.appInit() hook showStatus(msg='', err=null) { // retain the last known msg for this feature's AsyncInit this.statusMsg = msg; // retain the last known error for this feature's AsyncInit // ... done so in a "sticky" way so as to NOT clobber existing error if (err) { // retain latest error (when supplied) ... retaining prior err (when NOT supplied) this.err = err; } // if our process is being actively monitored, pass through to the app-specific callback function if (this.monitored) { this.showStatusApp(msg, this.err); } } } //*------------------------------------------------------------------------------ //* feature-life-cycle-hook: appDidStart(fassets, activeFeatures, additionalHooksParams): void //*------------------------------------------------------------------------------ op.flch.appDidStart = function(fassets, activeFeatures, additionalHooksParams) { // maintain: running counter of execution order of life-cycle-hooks (unit-test related) op.flch.appDidStart.executionOrder = executionOrder++; // log summary const hookCount = activeFeatures.reduce( (count, feature) => feature.appDidStart ? count+1 : count, 0); const hookSummary = activeFeatures.map( (feature) => `\n Feature.name:${feature.name}${feature.appDidStart ? ' <-- defines: appDidStart()' : ''}` ); logf(`feature-life-cycle-hook ... PROCESSING: Feature.appDidStart() ... ${hookCount} hooks:${hookSummary}`); // apply Feature.appDidStart() life-cycle hooks activeFeatures.forEach( feature => { if (feature.appDidStart) { logf(`feature-life-cycle-hook ... Feature.name:${feature.name} ... invoking it's defined Feature.appDidStart()`); feature.appDidStart({fassets, ...additionalHooksParams}); } }); };