Skip to content

feature-u Enhancement: Rebase Active Features at run-time #27

@KevinAst

Description

@KevinAst

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:

    The caveat here is that any newly activated feature (resulting from the "rebase" operation) must either:

    • only provide fasset definitions
    • or be part of the original set of active features at startup (via launchApp()).

    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:

    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:

    • 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)

    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).

    • 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 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

    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:

    • isStartup: a boolean indicating if the app is being started. When true we know that launchApp() is in control (one time only). When false then rebase() is driving the process (can be many times).

    • enablementCtx: additional enablement context supplied by the application through launchApp() and rebase() parameters.

launchApp()

    The launchApp() function has a slightly refined signature:

    + launchApp({..., [enablementCtx]}): FeatureCtx 

    Params:

    • enablementCtx: a new optional parameter that provides the enablement context to the enablementFn(). As an example, this could be the authenticated User object.

    Returns:

    • a FeatureCtx object (see below). Previously launchApp() returned a fassets object.

FeatureCtx

    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:

    • getFassets(): fassets returns the current fassets object (the original return from launchApp())

    • hasFeature(featureName): boolean: determines if a feature is present or not (originally part of the fassets object)

    FeatureCtx: { + getFassets(): fassets + hasFeature(featureName): boolean }

rebase()

    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:

    • featureCtx: the FeatureCtx object (discussed above), obtained from launchApp(), which identifies the overall set of app features.

    • enablementCtx: the optional parameter that provides the enablement context to the enablementFn(). As an example, this could be the authenticated User object.

    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 enabled directive (as always):

    feature.js

    export default createFeature({ name: 'eateries', fassets, reducer, logic, route, appInit, });
  • The eateryServiceMock feature only contains fassets definitions, 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 the isStartup directive.

    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 featureCtx for subsequent use

    • does not supply the enablementCtx because there is no authenticated user at 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 featureCtx from the return of launchApp() (in app.js).

    • it supplies the enablementCtx because there is an authenticated user at this time.

    import featureCtx from 'app'; ... rebase({featureCtx, enablementCtx: user}); ...

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions