Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
KevinAst opened this issue Feb 7, 2020 · 5 comments
Open

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

KevinAst opened this issue Feb 7, 2020 · 5 comments
Labels
enhancement New feature or request

Comments

@KevinAst
Copy link
Owner

KevinAst commented Feb 7, 2020

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});
    ...
@KevinAst KevinAst added the enhancement New feature or request label Feb 7, 2020
@KevinAst
Copy link
Owner Author

KevinAst commented Feb 9, 2020

@sylvainlg ... you may have interest in this ... comments are welcome :-)

@sylvainlg
Copy link

Wow this is a really interesting RFC.
I can see some usages to this but i also worry about the implementation complexity.

Uses :

  • Network based feature flipping
  • User profil based feature enablement
  • Enabling/disabling features when user change offer level (edge case)
  • Enabling/disabling feature on user's opt in/out

I have some questions :

  • How the aspects react to a rebase ?
  • How ensure define/use contract react to a rebase ? This might implies a huge combinatory complexity for complete testing
  • How redux and react-navigation would take the adds and removes of there assets ? Can the app keep working without problem when these changes occurs at run time ?

@sylvainlg
Copy link

Some of my co-workers asked me about a related subject.
Is feature-u can work along with React.lazy ?

@KevinAst
Copy link
Owner Author

Thanks for your questions @sylvainlg.

Regarding question: "How aspects react to a rebase" ... A: they would not (i.e. nothing would change). Aspect interaction only occurs during the startup process (via launchApp()), and has nothing to do with rebase().

Regarding question: "How define/use contract react to a rebase?" ... A: the same way they always have (i.e. nothing would change). A rebase() merely regenerates the fassets defined from the new set of active features ... nothing more. Each feature would generate their fassets in the same way they always have. The only variation is the potential change in the set of active features.

Regarding questin: "How redux and react-navigation would take the adds and removes of there assets?" ... A: Because rebase() does not reconfigure your frameworks, technically this configuration would still be in place. The idea is they would become logically "dormant", because they would not be triggered by inactive feature probes. Remember, rebase() will enforce that the new set of active features would NOT introduce anything (other than fassets) that hadn't been seen by launchApp() (see the proposal's Caveat section of "The Solution"). I think this is the only complexity that is added to the system (i.e. features MUST be initially enabled if they are configuring frameworks, because this would not be allowed at rebase time).

Regarding complexity, my goal would to make rebase completely transparent to both features and aspect plugins ... allowing the system operate in the same philosophy it always has.

    I think this can be accomplished if your features are properly designed ... loosely coupled, using autonomous injection ... a pull philosophy that makes plug-and-play possible.

    For those who are not familiar with this, it is discussed in Resource Contracts and is shown in the Video Presentation at:

    • Time 23:03 - Plug-and-Play Sneak Peek
    • Time 50:58 - UI Composition
    • Time 53:16 - PULL: Usage Contract

    I would encourage anyone to view the video in it's entirety :-)

Hope this helps!

Thoughts?

P.S. Regarding React.lazy and Suspense, I have no experience with this (sorry). I would tend to think it should work, but you may want to try a simple test.

FYI: I have no immediate plans to implement this design (I am very busy on another project). At this point, I just wanted to get the design out for feedback.

@ScreamZ
Copy link

ScreamZ commented Dec 2, 2020

Really interested in this one too.

Currently what I'm doing is pretty straight forward.

I have an app with some additional services that can be purchased. The module is active all time and using a fasset and a feature module "firebase remote config" i can say whether the feature is active or not using the fassets.

@KevinAst KevinAst mentioned this issue Mar 20, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

3 participants