Skip to content

Design doc :: Vuex Macros for HaxeVX

Glenn Ko edited this page Feb 23, 2017 · 149 revisions

Status: Implemented since haxelib version 0.6

Design doc: How Vuex store/modules macro-generation work?

HaxeVx for Vuex :: Interfaces that generate types and validates classes by macro:

In pseudo code examples, for functions with arguments enclosed with {}, type signatures should be manually left out in function arguments, so that macro will inject in specific types manually (typing it out manually is possible but will be checked against expected types, anyway)

For class/interface type-parameters (those in capitalized letters) that are non-applicable in certain cases, you can mark them with a NoneT empty marker class, in order to prevent any form of direct access on those types.

Note: Haxe build/autoBuild macros are executed live while coding in your editor When such Haxe macros run, any generated fields/methods by the macros are reflected in Intellisense immediately, giving you full-type hinting support right off the bat, not just compile-time checking.

Within IModule<S,RS>/VModule<S,RS>

An IModules class includes a macro that consolidates any relavant static class methods as getter functions for Vuex, including it's state reference and any other annotated fields for @:mutator, @:action @:module, etc.

It also auto-generates the public read-only getter fields from the static getter methods, with matching types.

Note that VModule is simply a bare-bones boilerplate class that implements IModule.

Required type parameters:

  • S: The local state type
  • RS: The root state type

Within the IModule/VModule class, your context-specific getters are declared as static functions to retrieve given value from state parameter.

var state:S;

// min 1 parameter required for all Getter functions.
public static function Get_someValue(state:{macro-checked against S}) {
   
}  	

 public static function Get_something(state:{macro-checked against S}, getters:???) {
   
}  	

public static function Get_somethingsWhileReferingToRoot(state:{macro-checked against S}, getters:???, rootState:{macro-checked against RS}) {
   
}  	

// @:namespaced class style declarations below.

// If VModule/IModule class for module is explcitly marked as @:namespaced,
// due to enforced vuex module @:namespaced setting for this class, getters can automatically refer to local module context.
public static function Get_somethingNamespaced(state:{macro-checked against S}, getters:{macro-generated anon typedef of localised getter fields}, rootState:{macro-checked against RS}, rootGetters:???)) {
   
}   	

Such static functions can be public as well in order to be used in other contexts. Furthermore, you can also declare them as inline.

If you use the @:namespaced metadata on the given module class, Haxe will ALWAYS have such a module set with the vuex setting namespaced:true, always. This will cause all mutations, actions, etc. as such to be deemed already registered as namespaced on native Vuex's end.

Alternatively, instead of using a global @:namespaced annotation on the module class which would tie all your mutations/actions to be automatically prefixed by the module namespace path, you can instead manually further annotate individual @:mutator or @:action fields with @:useNamespacing instead. This HaxeVx only feature allows you to apply a mixture of mutations/actions, some of which may/may not be namespaced, allowing a module to respond to both module-specific dispatches/commits and root dispatches/commits. By default, all @:mutation and @:action fields under the module class are not namespaced, so that they can globally respond to root dispatches/commits. Thus, @:useNamespacing annotations allow you to manually control which mutations/actions are meant to respond from global scope versus module scope.

If you use @:useNamespacing annotations for individual fields, you CANNOT use @:namespaced annotation for your module class (ie. a compiler warning will warn you with: Annotation @:useNamespacing is ignored. Class is already @:namespaced.

Type limitation caveats within Get_ static function handlers:

  • The type for rootGetters parameter cannot be explicitly determined as it exists outside of the scope of the current module class.
  • The type for getters parameter for module classes that aren't annotated to @:namespaced cannot be explicitly determined from within the module itself (since in a non-namespaced situation, it would refer to the root store getters, which is outside the scope of the current module class).

Within IGetters<S, RS> classes

An IGetter class includes a macro that consolidates all it's static class methods as getter functions for Vuex/HaxeVx, and auto-generates public read-only getter fields from them with matching types.

Required type parameters:

  • S: The local state type
  • RS: The root state type

The static Getter functions are defined in similar format to IModule/VModule. Only such functions can be declared and nothing else. (ie. no state:S. @:mutator, @:action, @:module fields, etc.)

Within IStoreGetters<S> classes

It actually extends from IGetters via:

interface IStoreGetters extends IGetters<S, NoneT>

An IStoreGetters class includes a macro that consolidates all it's static class methods as getter functions for Vuex stores only (at root-level), and auto-generates plain public read-only property fields from them with matching types. If you are sure that your IGetters will only be used on the root store itself, use IStoreGetters instead for better optimization and brevity.

Required type parameters:

  • S: The local state type

IStoreGetters can only be declared on VxStore classes under the store.getters field, and no where else.

Within store getters, the getters parameter under Get_ static function handlers have no type limitation caveat, ie. the type signature for getters parameter is wired immediately to the local class context type itself.

Within IMutator<S> classes

An IMutatorclass includes a macro that consolidates all it's local class prototype private methods as mutator function handlers for Vuex, and auto-generates the matching helper public inline functions to commit the given mutations.

Required type parameters:

  • S: The local state type

Mutators are as simple as it gets.

function setSomething(state:{S}, payload??) {
   
}  	

And auto-generates the following matching helper method that is underscore _ prefixed:

public inline function _setSomething(context:IVxContext, payload??, useNamespacing:Bool=false, namespace:String="")
{
    if (useNamespacing) {
         context.commit(namespace+"macroGeneratedStringFromClass", payload);
    }
    else {
        context.commit("macroGeneratedStringFromClass", payload);
    }
}

Since the method is inlined, it doesn't actually gets called at runtime. useNamespacing is a Boolean constant flag to determine which inlined output branch to use, and so long as you supplied an explicit constant true/false in your function call) , the Haxe compiler can skip the if (useNamespacing) check and the compiled code results in a single line. If you assigned a boolean variable (instead of constant) for useNamespacing, though, it'll intelligently make the inlined if consideration based off the variable. Thus, in either case, there's no additional runtime overhead.

The only external dependency required to be supplied to the inlined method, is the current IVxContext, (which is available within component classes as store reference, or action handler functions as context parameter ). For more information on IVxContext, see the next section on IAction.

Within IAction<S, RS> classes

An IAction class includes a macro that consolidates all it's local class prototype private methods as action handlers for Vuex, and auto-generates the matching helper public inline functions to dispatch the given actions.

  • IVxContext is the base interface with the basic store commit and dispatch functions.
  • IVxContext1 extends IVxContext supports additional S type parameter for current context state. This interfaces is also implemented by VxStore by default.
  • IVxContext2 extends IVxContext1 supports additional G type parameter for current context getters, which might refer to the current module's getters if the module registering this action instance is using namespaced:true option, or root getters if the module isn't namespaced (by default).
  • IVxContext3 extends IVxContext2 supports additional RS type parameter for root state.
  • IVxContext4 extends IVxContext3 supports additional RG type parameter for root getters.

examples:

function doSomethingVerySimple(context:IVxContext, ?>payload:???) {}  	

function doSomethingReferCurState(context:IVxContext1{<S>}, ?>payload:???) { }  	

function doSomethingComplex(context:IVxContext3{<S,G?,RS>}, ?>payload:???) {}  	


function doSomethingNamespaced(context:IVxContext4{<S,G,RS,RG?>>}payload:???) { }   

The auto-generated public inline _ helper methods are spawned in similar fashion to IMutator implementation, but uses context.dispatch(..) instead.

Type limitation caveats:

  • The types for getters G parameter and rootGetters RG parameters in all circumstances cannot be explicitly determined.

  • Be aware that there is no compile-time macro to detect whether you accidentally set something on context.state or context.rootState within the action handler (this is hard to determine/write). This can only be detected effeciently at runtime if running Vuex in development (strict?) mode, which will check that you only commit mutations while processing actions. It's possible within Haxe to scan for AST expressions to determine if a = assignment is done on context.state or context.rootState, but doing so indirectly through another variable reference (or another "magic" function within context.state) can be used to circumvent such checks.

An alternative to Vuex getters:

For situations within G/RG getters which can't safely+accurately resolve their given types , an alternative approach is to simply call ANY class's static Getter functions and pass in the relavant state parameter. In fact, doing so might yield better performance (particular if the static functions are inlined) instead of relying on the dynamic ES5 getter approach from Vuex getters. In fact, why do we really need dynamically registered Vuex-style getters anyway in a Haxe environment, when you already have static inlinable class functions?

Also, this same practice can be adopted in regular Javascript by importing/refering to whatever handy getter functions you have in your code library to retrieve values from a given state.

In short, it might be a better practice to enforce either Dynamic or NoneT types for unknown types of getter/rootGetter, rather than assume with a given type, which might lead to runtime errors if you assumed the wrong type in your Haxe codebase. The current Typescript definitions for Vuex has "getters"-related fields typed to Dynamic any by default. For HaxeVx, might consider an optional compile flag like D vue_no_untyped_getters to use NoneT instead on such unknown getters, forcing coders to rely on actual Getter(state) functions instead.


How macros reflect native object parameters to Vuex

IMutator/IAction

Will generate internal :

// to inject into obj definition of mutators/actions:{} respectively:
public function _SetInto(obj:Dynamic, namespace:String){}

IModule/VModule/IGetters

All getters are are defined as private static function format as below:

static function Get_getterName(state:T):? {  return state.blabhlabh }

Which will generate the following fields by macro:

function get_getterName():? {  
   return untyped this._stg[_+"getterName"]; // "_stg is injected as store.getters, once nativeStore is created for "store".
}
public var getterName(get, never):?

together with:

public var _(default,never):String; //  macro-generated field for namespace prefix reference of module.

The Haxe getter fields with get_ function calls will perform namespaced getter request using _ namespace reference and locally injected native Vuex $store.getters.

When you call store.someModule.path.getterName, this will call method this.$store.someModule.path.get_getterName() at runtime. All namespacing is done behind the scenes already.

All IStoreGetters

All store-only getters are are defined as private static function format as below:

static function Get_getterName(state:T):? {  return state.blabhlabh }

Which will generate the following field by macro:

public var getterName(default,never):?;  // direct access

All IModule/VModule/VxStore

Internal initialization functions (tailored to each class) will be macro-generated for each Stores and Modules classes.

For VxStore, a macro-generated call to Init will automatically be added to the end of it's constructor as _Init(""), so initialization occurs immediately (including all it's sub-modules) upon instantiation of a VxStore implementation:

public function _Init(ns:String ) {
   _ = ns;

      #if (hasLocalClassMetadata(":namespaced")) { 
          untyped this.namespaced = true;
          // will be namespaced by Vuex, so do NOT apply HaxeVx namespacing
          var useNS:String = "";
      }
      #else {
           var useNS:String =  ns;  // apply namespacing manually
      }
      #end

	var d;
	var cls:Dynamic = ThisClass;
	var clsP:Dynamic = cls.prototype;
            var key:String;
	  
   // if got getters under local class module/store,  
	d = {}; untyped this.getters = d; 
	
             // For each "getterField" in local class getters  or in store's "getter" field class.
             // set up parameters over def with given namespace to static getter functions
             key = useNS+"getterField";
	untyped d[key] = cls.Get_getterField; 
	// register field globally to GettersToInjectOf(this, ns, key)

             // For any @:getter field add-ons ...
             // if got getter add-ons,  instantiate set up parameters over def with given namespace 
             if (getterBuildFieldName == null) {
		  getterBuildFieldName = new GetterModuleInstance();  // assumes no parameter in constructor.
	   }
              cls= ClassOfGetter;
                // For each "getterField" in `cls` getters  
               key = useNS+"getterField"+getSuffix(ClassOfGetter);
	  untyped d[key] = cls.Get_getterField; 
              // register field globally to GettersToInjectOf(getterBuildFieldName , ns + "|"+getterBuildFieldName, key)
   // ...



             // For any @:mutator field add-ons...
	// if got mutators, set up parameters over def with given namespace if required
	d = {}; untyped this.mutators = d; 
            if (mutatorBuildFieldName == null) {
		  mutatorBuildFieldName = getSingletonOf(MutatorClass);  // assumes no parameter in constructor.
	   }
	mutatorBuildFieldName._SetInto(d, #if(useNamespacing(buildField)) useNS #else "" #end ) ); 

   // ...

             // For any @:action field add-ons...
	 // if got actions, set up parameters over def with given namespace  if required
	d = {}; untyped this.actions = d;
	  if (actionBuildFieldName == null) {
		  actionBuildFieldName = getSingletonOf(ActionClass);  // assumes no parameter in constructor.
	   }
                actionBuildFieldName ._SetInto(d, #if(useNamespacing(buildField)) useNS #else "" #end ) ); 
   // ...

          // whether to use `useNS` setting or not is dependent on it's build field having @:useNamespacing annotations or not. 

            // For any @:module field add-ons
	// if got modules, set up parameters over def with given namespace 
	d = {}; untyped this.modules = d;
	// will auto-instnatite module by default, unless marked with @:manual metadata on module field (which is a good way to prevent infinite recursion and mark module fields that might conditionally be null).
	   // #may not occur if field has @:manual metadata included in
	  if (moduleBuildFieldName == null) {
		  moduleBuildFieldName = new ModuleInstance();  // assumes no parameter in constructor.
	   }
	   // #end

	  if (moduleBuildFieldName != null) {
		d.moduleBuildFieldName = moduleBuildFieldName; 
		moduleBuildFieldName._Init(ns+moduleBuildFieldName+"/");  // warning, this is a recursive function call approach 
	 }
   // ...
}


public inline function _InjNative(nativeStoreGetter:Dynamic) {
  untyped this._stg = nativeStoreGetter;
      untyped this.state._ = _;
}

(HaxeVx only feature) Module Namespaced Actions/Mutator/Getters:

This feature is only available in HaxeVx, and not in regular Vuex ie. the ability to access modules' namespaces and getters directly from the given store reference while coding within components, with full strict-typed Intellisense.

A module getter can be quickly accessed from store via something like store.someModule.aModule.bModule.getterName.

Likewise, it's recorded namespace of module can be easily accessed with underscore property of module, like store.someModule.aModule.bModule._.

If a particular state class implements a IPrefixedState interface or includes an assosiated build macro to include in a readonly underscore _:String property, the property can be accessed from within the store's state tree, so long as the modules' associated states are somehow listed within the state tree as well. eg. store.state.someModule.aModule.bModule._.

Namespace can be used as last parameter in macro-generated inline (Action/Mutator reference)._commitStr(store, {?payload:?}, useNamespace:Bool=false, namespace:String=""}) function. This allows you to commit mutations or dispatch actions with module namespaces in a strict typed manner elegantly. (eg. (Action/Mutator reference)._commitStr(store, true, store.someModule.aModule.bModule._) ), and is good for specifically targeting mutations/actions that will definitely only affect a certain module. Also note that due to Haxe inlining, the (Action/Mutator reference) can also be null, it won't matter anyway as no instance function is actually called anyway.

Post-Vuex.Store instantiation dependency injection.

By default, the intention is to keep HaxeVx+Vuex runtime initialization to the minimum in production/deployment mode. But with the HaxeVx-only features mentioned above, these are the only 2 additional post-construct processes required :

  1. Once native Vuex.Store is created from VxStore definition instance, run through global linear stack of collected initialized modules, and call their InjNative(nativeStore.getters) functions. This will inject the native store.getters reference as a this._stg property. Clear the stack to an empty array so it can be re-used later for any dynamically registered modules.
  2. Also, inject modules from VxStore definition instance into native Vuex store instance, so module getter helper methods and helper _ namespace variables can be accessed from the native Vuex store itself.

Note that if you dynamically register a module, something like Step 1-2 would happen as well to link those newly added modules up to Vuex.

Debug/development mode (ie. non-production compile):

Of course, more runtime checks will be done in development mode:

  • All mutators/action classes will be regsitered as singletons when initializing VxStore as well during _Init of VxStore and it's modules recursively.
  • Component requesting mutators/action classes via @:mutator/@:action metadata will attempt to request for singleton, and will show a warning if singleton not found in registry.