@@ -23,7 +23,7 @@ const { REDIS_INTEGRATION_MODE } = redis;
2323const { Logger } = require ( "./logger" ) ;
2424const cfEnv = require ( "./env" ) ;
2525const { HandlerCollection } = require ( "./shared/handlerCollection" ) ;
26- const { ENV , isObject, tryRequire } = require ( "./shared/static" ) ;
26+ const { ENV , isObject, tryRequire, tryPathReadable } = require ( "./shared/static" ) ;
2727const { promiseAllDone } = require ( "./shared/promiseAllDone" ) ;
2828const { LimitedLazyCache } = require ( "./shared/cache" ) ;
2929const { Semaphore } = require ( "./shared/semaphore" ) ;
@@ -39,9 +39,16 @@ const SCOPE_KEY_INNER_SEPARATOR = "::";
3939const SCOPE_KEY_OUTER_SEPARATOR = "##" ;
4040const SCOPE_ROOT_KEY = "//" ;
4141
42+ const CONFIG_SOURCE = Object . freeze ( {
43+ RUNTIME : "RUNTIME" ,
44+ FILE : "FILE" ,
45+ AUTO : "AUTO" ,
46+ } ) ;
47+
4248const CONFIG_KEY = Object . freeze ( {
4349 TYPE : "TYPE" ,
4450 ACTIVE : "ACTIVE" ,
51+ SOURCE : "SOURCE" ,
4552 APP_URL : "APP_URL" ,
4653 APP_URL_ACTIVE : "APP_URL_ACTIVE" ,
4754 VALIDATIONS : "VALIDATIONS" ,
@@ -52,6 +59,7 @@ const CONFIG_KEY = Object.freeze({
5259const CONFIG_INFO_KEY = {
5360 [ CONFIG_KEY . TYPE ] : true ,
5461 [ CONFIG_KEY . ACTIVE ] : true ,
62+ [ CONFIG_KEY . SOURCE ] : true ,
5563 [ CONFIG_KEY . APP_URL ] : true ,
5664 [ CONFIG_KEY . APP_URL_ACTIVE ] : true ,
5765 [ CONFIG_KEY . VALIDATIONS ] : true ,
@@ -103,7 +111,7 @@ class FeatureToggles {
103111
104112 _processValidations ( featureKey , validations , configFilepath ) {
105113 const workingDir = process . cwd ( ) ;
106- const configDir = configFilepath ? path . dirname ( configFilepath ) : __dirname ;
114+ const configDir = configFilepath ? path . dirname ( configFilepath ) : workingDir ;
107115
108116 const validationsScopesMap = { } ;
109117 const validationsRegex = [ ] ;
@@ -177,17 +185,41 @@ class FeatureToggles {
177185 }
178186 }
179187
180- /**
181- * Populate this.__config.
182- */
183- _processConfig ( config , configFilepath ) {
188+ _processConfigSource ( source , configFromSource , configFilepath ) {
189+ let count = 0 ;
190+ if ( ! isObject ( configFromSource ) ) {
191+ return count ;
192+ }
193+
184194 const { uris : cfAppUris } = cfEnv . cfApp ;
185- const configEntries = Object . entries ( config ) ;
186- for ( const [ featureKey , { type, active, appUrl, fallbackValue, validations } ] of configEntries ) {
195+ const entries = Object . entries ( configFromSource ) ;
196+ for ( const [ featureKey , value ] of entries ) {
197+ if ( this . __config [ featureKey ] ) {
198+ continue ;
199+ }
200+ count ++ ;
201+
202+ if ( ! isObject ( value ) ) {
203+ throw new VError (
204+ {
205+ name : VERROR_CLUSTER_NAME ,
206+ info : {
207+ featureKey,
208+ source,
209+ } ,
210+ } ,
211+ "feature configuration is not an object"
212+ ) ;
213+ }
214+
215+ const { type, active, appUrl, fallbackValue, validations } = value ;
216+
187217 this . __featureKeys . push ( featureKey ) ;
188218 this . __fallbackValues [ featureKey ] = fallbackValue ;
189219 this . __config [ featureKey ] = { } ;
190220
221+ this . __config [ featureKey ] [ CONFIG_KEY . SOURCE ] = source ;
222+
191223 if ( type ) {
192224 this . __config [ featureKey ] [ CONFIG_KEY . TYPE ] = type ;
193225 }
@@ -210,8 +242,23 @@ class FeatureToggles {
210242 }
211243 }
212244
245+ return count ;
246+ }
247+
248+ /**
249+ * Populate this.__config.
250+ */
251+ _processConfig ( [ configRuntime , configFromFile , configAuto ] , configFilepath ) {
252+ const configRuntimeCount = this . _processConfigSource ( CONFIG_SOURCE . RUNTIME , configRuntime , configFilepath ) ;
253+ const configFromFileCount = this . _processConfigSource ( CONFIG_SOURCE . FILE , configFromFile , configFilepath ) ;
254+ const configAutoCount = this . _processConfigSource ( CONFIG_SOURCE . AUTO , configAuto , configFilepath ) ;
255+
213256 this . __isConfigProcessed = true ;
214- return configEntries . length ;
257+ return {
258+ [ CONFIG_SOURCE . RUNTIME ] : configRuntimeCount ,
259+ [ CONFIG_SOURCE . FILE ] : configFromFileCount ,
260+ [ CONFIG_SOURCE . AUTO ] : configAutoCount ,
261+ } ;
215262 }
216263
217264 _ensureInitialized ( ) {
@@ -641,43 +688,45 @@ class FeatureToggles {
641688 ) ;
642689 }
643690
644- /**
645- * Initialize needs to run and finish before other APIs are called. It processes the configuration, sets up
646- * related internal state, and starts communication with redis.
647- */
648- async _initializeFeatures ( { config : configInput , configFile : configFilepath = DEFAULT_CONFIG_FILEPATH } = { } ) {
691+ async _initializeFeatures ( { config : configRuntime , configFile : configFilepath , configAuto } = { } ) {
649692 if ( this . __isInitialized ) {
650693 return ;
651694 }
652695
653- let config ;
696+ let configFromFile ;
654697 try {
655- config = configInput ? configInput : await FeatureToggles . readConfigFromFile ( configFilepath ) ;
698+ if ( ! configFilepath && ( await tryPathReadable ( DEFAULT_CONFIG_FILEPATH ) ) ) {
699+ configFilepath = DEFAULT_CONFIG_FILEPATH ;
700+ }
701+ if ( configFilepath ) {
702+ configFromFile = await FeatureToggles . readConfigFromFile ( configFilepath ) ;
703+ }
656704 } catch ( err ) {
657705 throw new VError (
658706 {
659707 name : VERROR_CLUSTER_NAME ,
660708 cause : err ,
661709 info : {
662710 configFilepath,
663- ...( configInput && { configBaseInput : JSON . stringify ( configInput ) } ) ,
664- ...( config && { configBase : JSON . stringify ( config ) } ) ,
665711 } ,
666712 } ,
667- "initialization aborted, could not resolve configuration "
713+ "initialization aborted, could not read config file "
668714 ) ;
669715 }
670716
671- let toggleCount ;
717+ let toggleCounts ;
672718 try {
673- toggleCount = this . _processConfig ( config , configFilepath ) ;
719+ toggleCounts = this . _processConfig ( [ configRuntime , configFromFile , configAuto ] , configFilepath ) ;
674720 } catch ( err ) {
675721 throw new VError (
676722 {
677723 name : VERROR_CLUSTER_NAME ,
678724 cause : err ,
679725 info : {
680- ...( config && { config : JSON . stringify ( config ) } ) ,
726+ ...( configAuto && { configAuto : JSON . stringify ( configAuto ) } ) ,
727+ ...( configFromFile && { configFromFile : JSON . stringify ( configFromFile ) } ) ,
728+ ...( configRuntime && { configRuntime : JSON . stringify ( configRuntime ) } ) ,
729+ configFilepath,
681730 } ,
682731 } ,
683732 "initialization aborted, could not process configuration"
@@ -751,16 +800,25 @@ class FeatureToggles {
751800 }
752801 }
753802
803+ const totalCount =
804+ toggleCounts [ CONFIG_SOURCE . RUNTIME ] + toggleCounts [ CONFIG_SOURCE . FILE ] + toggleCounts [ CONFIG_SOURCE . AUTO ] ;
754805 logger . info (
755- "finished initialization with %i feature toggle%s with %s" ,
756- toggleCount ,
757- toggleCount === 1 ? "" : "s" ,
806+ "finished initialization with %i feature toggle%s (%i runtime, %i file, %i auto) with %s" ,
807+ totalCount ,
808+ totalCount === 1 ? "" : "s" ,
809+ toggleCounts [ CONFIG_SOURCE . RUNTIME ] ,
810+ toggleCounts [ CONFIG_SOURCE . FILE ] ,
811+ toggleCounts [ CONFIG_SOURCE . AUTO ] ,
758812 redisIntegrationMode
759813 ) ;
760814 this . __isInitialized = true ;
761815 return this ;
762816 }
763817
818+ /**
819+ * Initialize needs to run and finish before other APIs are called. It processes the configuration, sets up
820+ * related internal state, and starts communication with redis.
821+ */
764822 async initializeFeatures ( options ) {
765823 return await Semaphore . makeExclusiveReturning ( this . _initializeFeatures . bind ( this ) ) ( options ) ;
766824 }
0 commit comments