import Bucketer from './Bucketer' ; import AudienceEvaluator from './AudienceEvaluator' ; import Config , { Datafile , Feature } from './Config' ; import Storage from './Storage' ; class Engine { static readonly TOTAL_BUCKETS = 10000 ; static readonly TRAFFIC_ALLOCATION_SALT = 'tas' ; private config : Config ; private bucketer : Bucketer ; private evaluator : AudienceEvaluator ; private userId ? : string ; private attributes ? : Record < string , any > ; private storage ? : Storage < string > ; private cache : { [ id ] : string ; } = { } ; constructor ( { datafile , userId , attributes , storage } ) { this . config = new Config ( datafile , Engine . TOTAL_BUCKETS ) ; this . bucketer = new Bucketer ( Engine . TOTAL_BUCKETS ) ; this . evaluator = new AudienceEvaluator ( ) ; this . storage = storage ; this . userId = userId ; this . attributes = attributes ; } private computeKey ( id , userId = '' , salt = '' ) { return ( userId || this . userId || '' ) . concat ( id ) . concat ( salt ) ; } private getForcedVariation ( experimentId ) { return this . cache [ experimentId ] ; } getUserId ( ) { return this . userId ; } getAttributes ( ) { return this . attributes ; } setUserId ( userId ) { this . userId = userId ; } setAttributes ( attributes = { } ) { this . attributes = attributes ; } setForcedVariation ( experimentId , variationId ) { this . cache [ experimentId ] = variationId ; } isFeatureEnabled ( featureId , userId ? , attributes ? ) { const key = this . computeKey ( featureId , userId ) ; const feature = this . config . getFeature ( featureId ) ; if ( ! feature ) { return null ; } const { audience } = feature ; const allocation = this . config . getFeatureAllocation ( featureId ) ; if ( ! allocation || ! this . evaluator . evaluate ( audience , attributes || this . attributes ) ) { return null ; } return ! ! this . bucketer . bucket ( key , [ allocation ] ) ; } getFeature ( featureId ) { return this . config . getFeature ( featureId ) ; } getEnabledFeatures ( userId ? , attributes ? ) { const features = this . config . getFeatures ( ) ; return Object . keys ( features ) . reduce ( ( features , featureId ) => { return { ... features , [ featureId ] : this . isFeatureEnabled ( featureId , userId , attributes ) } ; } , { } ) ; } getVariationId ( experimentId , userId ? , attributes ? ) { let variationId = this . getForcedVariation ( experimentId ) || this . storage ?. get ( experimentId ) ; if ( variationId ) { return variationId ; } const experiment = this . config . getExperiment ( experimentId ) ; if ( ! experiment ) { return null ; } const { audience } = experiment ; if ( ! this . evaluator . evaluate ( audience , attributes || this . attributes ) ) { return null ; } let key = this . computeKey ( experimentId , userId , Engine . TRAFFIC_ALLOCATION_SALT ) ; const allocation = this . config . getExperimentAllocation ( experimentId ) ; if ( ! allocation || ! this . bucketer . bucket ( key , [ allocation ] ) ) { return null ; } key = this . computeKey ( experimentId , userId ) ; const allocations = this . config . getExperimentAllocations ( experimentId ) ; variationId = this . bucketer . bucket ( key , allocations ) ; this . storage ?. store ( experimentId , variationId ) ; return variationId ; } getVariationIds ( userId ? , attributes ? ) { const experiments = this . config . getExperiments ( ) ; return Object . keys ( experiments ) . reduce ( ( experiments , experimentId ) => { return { ... experiments , [ experimentId ] : this . getVariationId ( experimentId , userId , attributes ) } ; } , { } ) ; } } export default Engine ;