- Notifications
You must be signed in to change notification settings - Fork 6
Description
RFC (Request for Comments)
This design is an initial draft of a feature-u enhancement that will:
Allow the set of active features to change after the app is fully "up and running"!!!
This design has been published here in order to solicit feedback from you. Please leave your insight in the comments section below. Your help in improving feature-u is greatly appreciated!
At a Glance
The Need
One of the most powerful aspects of feature-u is it's ability to dynamically determine the set of active features (via Feature Enablement). This, in conjunction with Cross Feature Collaboration, enables true plug-and-play capabilities ... where the mere existence of a feature exudes the characteristics it implements.
While this is indeed a very powerful capability, it currently has a limitation ... that is the determination of "active features" is accomplished only once throughout the entire life cycle of the app ... at startup time (via launchApp()).
We need an ability to change the active features at run-time.
As an example of this, consider that different users should be able to manifest a distinct feature set, based on their enablement. A "guest" user will operate under a "bare minimum" feature set, while a "licensed" user will benefit from more advanced features.
The problem in this scenario is that the user in question is determined after the app has started ... once authentication has completed.
There needs to be a way to "rebase" the active features after the app has started (i.e. at run-time)!
The Issue
In thinking about this, a broad solution of "rebasing" features can be difficult. The issue is that the initial set of active features (from launchApp()) determines very low-level characteristics ... such as framework configuration.
Take redux state management as an example. Once the app is up and running, it would be difficult to introduce additional state from the activation of a new feature (one that was not already configured at startup).
Presumably, this would be an issue for other frameworks as well.
There is a lot of to consider here, and I am fearful of introducing needless complexity. From a broad perspective, if we wanted to rebase every characteristic of a feature, I am not completely sure how this could be reliably accomplished.
The Solution (potential)
In my experience, a "rebasing" of active features that only recalculates fassets would go a long way to solving this problem, and could be accomplished fairly easily.
Background:
fassets are the public API of features, and are the mechanism by which Cross Feature Collaboration is accomplished in an extendable way.
Caveat:
- only provide
fassetdefinitions - or be part of the original set of active features at startup (via
launchApp()).
The caveat here is that any newly activated feature (resulting from the "rebase" operation) must either:
Any other scenario would error out, because we are not supporting the rebase of things like frameworks.
Example:
As an example, if an "advanced" feature was exposed by promoting it's menu item through autonomous injection (a fassets Resource Contract), then when that feature was activated/deactivated, it would dynamically appear/disappear.
I have run across this exact scenario in my feature-based applications.
Mitigated Limitations:
-
it may represent a more encompassing application design issue (say an inappropriate coupling)
-
even if this is not the case, there are work-arounds (for example, conditional logic based on whether self's feature is active or not)
-
GREAT: normally, the monitored actions are internal to a feature, and so the actions would never be emitted/consumed.
-
However, consider external actions that are monitored by this deactivated feature.
-
GREAT: inactive external features would never emit actions that are being monitored, so "no harm no foul".
-
PROBLEM: however if active external features emit actions that are being monitored, then the logic from our "inactive" feature would incorrectly execute. The ramification of this would vary and ultimately be application specific. As mentioned above, this can be mitigated with additional application logic.
-
I can conceive of scenarios where this limited "rebase" approach could have the potential of being problematic.
With that being said, I have not seen this in practice (I believe it to be a low probability). My current thought is that if these scenarios do in fact occur:
Consider redux-logic, where logic modules monitor redux actions. If our "rebase" operation deactivated a feature that contained logic, then technically that logic would still be active. This is because logic is "bound" to it's monitored actions very early, at start-up time (i.e. launchApp()). In the case of monitoring external actions (from other features) this is accomplished through Cross Feature Collaboration using expandWithFssets() (see Managed Code Expansion).
I know this is an "involved" topic, but any thoughts?
The Proposal
I propose the introduction of a rebase() function, which can be invoked anytime during the application life cycle. This function will re-analyze the set of active features, and result in an updated fassets object, reflecting the fassets promoted from the current active set of features.
With this enhancement, the fassets object can now actually change for the first time. As a result, there are some minor tweaks in how it is accessed.
API
Here is "rough cut" of the proposed API (some new, some changed):
Feature.enabled
The Feature enabled flag (see Feature Enablement) can now either resolve to a boolean (as always) or be an enablementFn (new).
+ Feature.enabled: boolean | enablementFn enablementFn
-
isStartup: a boolean indicating if the app is being started. Whentruewe know thatlaunchApp()is in control (one time only). Whenfalsethenrebase()is driving the process (can be many times). -
enablementCtx: additional enablement context supplied by the application throughlaunchApp()andrebase()parameters.
The enablementFn is used when additional context is needed in the determination of the active feature set. The function is invoked by feature-u, and has the following signature:
+ enablementFn(isStartup, enablementCtx): boolean Params:
launchApp()
enablementCtx: a new optional parameter that provides the enablement context to theenablementFn(). As an example, this could be the authenticated User object.- a
FeatureCtxobject (see below). PreviouslylaunchApp()returned afassetsobject.
The launchApp() function has a slightly refined signature:
+ launchApp({..., [enablementCtx]}): FeatureCtx Params:
Returns:
FeatureCtx
-
getFassets(): fassetsreturns the currentfassetsobject (the original return fromlaunchApp()) -
hasFeature(featureName): boolean: determines if a feature is present or not (originally part of thefassetsobject)
The FeatureCtx object is returned from launchApp().
For the most part, this is an opaque object, holding internal information about the app's feature set, and is used to seed the new rebase() function.
It does have some public API:
FeatureCtx: { + getFassets(): fassets + hasFeature(featureName): boolean }rebase()
-
featureCtx: theFeatureCtxobject (discussed above), obtained fromlaunchApp(), which identifies the overall set of app features. -
enablementCtx: the optional parameter that provides the enablement context to theenablementFn(). As an example, this could be the authenticated User object.
A new rebase() function is introduced:
This function can be invoked as needed any time after launchApp() has completed, and will "rebase" the set of active features, defined from the supplied featureCtx (obtained from launchApp()).
+ rebase({featureCtx, [enablementCtx]}): void Params:
The result of this invocation will be a new rendition of the fassets object (found in featureCtx.getFassets()), which is used in all cross feature collaboration.
Not only will a new fassets be generated, but because this is a "React Context Object", the application display will dynamically refresh, reflecting the latest rendition of fassets, as determined by the new active feature set.
Nice!!
Example Usage
This is a modified example, taken from eatery-nod-w:
-
The discovery feature is only available to licensed users. However it must be initially activated in order to configure it's framework pieces:
feature.js
export default createFeature({ name: 'discovery', enabled: (isStartup, user) => isStartup || user.isLicensed(), fassets, reducer, logic, route, });
-
The eateries feature is always active, so it has no
enableddirective (as always):feature.js
export default createFeature({ name: 'eateries', fassets, reducer, logic, route, appInit, });
-
The eateryServiceMock feature only contains
fassetsdefinitions, so it can be activated/deactivated at any time, regardless of whether it was initially active at start-up. In other words it is not concerned with theisStartupdirective.feature.js
export default createFeature({ name: 'eateryServiceMock, enabled: (isStartup, user) => mocksInUse() || (user && user.isGuest()), fassets, });
-
The sandbox feature's enablement is controlled through an independent run-time expression:
feature.js
export default createFeature({ name: 'sandbox', enabled: inDevMode(), fassets, });
-
The
launchApp()invocation:-
exports the
featureCtxfor subsequent use -
does not supply the
enablementCtxbecause there is no authenticateduserat startup time
app.js
export default launchApp({ features, aspects, registerRootAppElm, });
-
-
Here is a
rebase()invocation:-
it can be invoked many times
-
it pulls in the
featureCtxfrom the return oflaunchApp()(in app.js). -
it supplies the
enablementCtxbecause there is an authenticateduserat this time.
import featureCtx from 'app'; ... rebase({featureCtx, enablementCtx: user}); ...
-