1.0.0-dev.0 • Published 4 years ago

@layered-tree/core v1.0.0-dev.0

Weekly downloads
-
License
MIT
Repository
github
Last release
4 years ago

GitHub license npm version build test

Welcome to @layered-tree/core 👋

✨ Introduction

Layered tree is an javascript solution ( framework agnostic ) that lets us create easily a full application Plugin architecture. We just have to attach some strategies at different moment of a life cycle application. Then we can subscribe on each of those declared strategies with a Plugin object, to provide those, some methods ( layer ) to use. And then fully override/enhance or totally rewire the tree of the application.

  • 🎁 Bonus : It's developed in Typescript and we have some strong inference between Strategy expectations and Plugin.

🙋‍♂️ The need

  • Make core applications with some plugin architecture for consumers.
  • Make white label application and share it.
  • Make overridable things, those can't be extends easily.

👷🏻‍♂️ Install

yarn add @layered-tree/core 

⚙️ How to ?

🛠 Usage

Imagine we want to create a core game with a character that have some random generate stats and possible actions to do. keep in mind this game is for all ages.

Contributor side:

1- Create strategies

import { Pipeline } from '@layered-tree/core';

// I create my Strategies Storage for a Character creation
const characterStrategiesStorage = {
    // Stats is the source type i want to pass through my pipeline.
    // { name: string } is some data i need to pass for build my stats
    'character.initial.randomInitialStats': new Pipeline<Stats, { name: string }>(),
    // Actions is the source type i want top pass through my pipeline.
    // { stats: Stats } is some data i need to pass for build my actions
    'character.initial.randomInitialActions': new Pipeline<Actions, { stats: Stats }>(),
};

2- Aggregate strategies

import { StrategyAggregator } from '@layered-tree/core';

// I instantiate a StrategyAggregator with my Strategies Storage
const characterStrategies = new StrategyAggregator('character', characterStrategiesStorage);

ℹ️ It's possible to create all application strategies in one aggregator, but as a God class is an anti pattern a god aggregator is also a bad idea. Prefer an aggregator by responsibility.

3- Place strategies in application workflow

const createCharacter = (name: string): Character => {
    // Add a pipeline on the initial stats generation result.
    const stats = characterStrategies.processStrategy
      ('character.initial.randomInitialStats')
      // getRandomInitialStats(name) is the source i want to pass through my pipeline.
      // { name } is some data i need to pass for build my stats.
      (getRandomInitialStats(name), { name });

    // Add a pipeline on the initial action generation result.
    const actions = characterStrategies.processStrategy
      ('character.initial.randomInitialActions')
      // getRandomInitialActions(stats) is the source i want to pass through my pipeline.
      // { stats } is some data i need to pass for build my actions.
      (getRandomInitialActions(stats), { stats });

    return { stats, actions };
};

4- Create LayeredTree mediator with strategies aggregators

import { LayeredTree } from '@layered-tree/core';

// We instantiate LayeredTree object with all strategies of my application flow.
const gameLayeredTree = new LayeredTree({ strategies: [characterStrategies] });

ℹ️ It's possible to register a strategyAggregator on the fly, but it's not recommended, for two reasons:

  • Because it can be confusable and dangerous for consumer at to have access or not access to a strategy at different step of application workflow.
  • And because of typescript static typing, strategyAggregator added on the fly would not be inferred.

5- Expose mediator.

// We just export the type of our LayeredTree instance for ours consumers.
// The LayeredTree instance can be also exported directly if it's needed that consumers manipulate it.
// (For create there own LayeredTree based on the main
// one, for example)
export type GameLayeredTree = typeof gameLayeredTree;

// Here i expose an entry function of my app that take in parameter an array 
// of LayeredTreePlugin.
export const init = async (plugins: LayeredTreePlugin<any>[] = []) => {
    // First we register all plugins
    gameLayeredTree.registerPlugins(plugins);

    // Then the scenario can start
    startScenario();
};

Consumer side:

Now imagine with this core game i want to create an age restricted version, with violence.

1- Create plugin

import { GameLayeredTree } from '@game/core'

// I create a plugin for the character creations
// => Here we declare our plugin with the static type GameLayeredTree 
// to have all inference of all strategies i can use on this app.
const characterPlugin = new LayeredTreePlugin<GameLayeredTree>('character');

2- Attach some middleware to the wanted pipeline with a strategy subscriptions.

import { addFireWithGunAction, addRobBankAction } from 'actions'

// I subscribe to my pipeline strategy 'character.initial.randomInitialActions'
// And i provide two middlewares that add actions intended to an adult audience.
// ( it can be chain subscriptions ) 
characterPlugin
  .strategySubscribe('character.initial.randomInitialActions')
    (addFireWithGunAction)
    (addRobBankAction);

ℹ️ It's possible to subscribe to all pipeline in one plugin, but as a God class is an anti pattern a god plugin is also a bad idea. Prefer a plugin by responsibility.

ℹ️ for more details about Middleware check Pipeline strategy section.

3- Launch application with plugins.

import { init } from '@game/core'

init([characterPlugin]);

=> And here it's done ! 🎉

ℹ️ check the full example here

It's even possible to try it ! 🎮

  • clone the monorepo and run yarn start:core:example:kid or yarn start:core:example:restrictedat root

📚 API References

Main references:

Strategies references:

Secondary references:

Tools


Main references:


LayeredTree:

Contributor

Class LayeredTree<SA, LT, LTO>

LayeredTree is the mediator of all the solution.

That is what to share, from app to consumers.

  • It will register strategies, Aggregators, plugins, and allow to process those.
  • It could be instantiate with some other LayeredTree instantiations.

    Then it's possible to create a master Mediator with some others base mediators. It's useful in case of multiple packages that compose an app and to override those individually, but also override them globally through the master Mediator.

Type parameters

Constructors:

new LayeredTree(options: LayeredTreeOption<SA, LT>): LayeredTree

Parameters

  • options:

    The options of LayeredTree

    LayeredTreeOption<SA, LT> (primitive: object)

    • strategies:

      • type: SA | StrategyAggregator[]

      Array of StrategyAggregator instances.

    • layeredTrees:

      Optionnal

      • type: LT | LayeredTree[]

      Array of LayeredTree instances.

Returns: LayeredTree

Methods:

  • processStrategy

    processStrategy(strategyName: string): unknown

    To process all StrategyLayer ( methods ) registered LayeredTree instance.

    • Parameters:

      • strategyName: string

        The name associate to the strategy to pick.

    • Returns: unknown

      The strategy picked's return.

  • registerPlugins

    registerPlugins(plugins: LayeredTreePlugin<any>[]): void

    Register some LayeredTreePlugin in LayeredTree Mediator.

    • Parameters:

    • Returns: void

  • registerStrategies

    registerStrategies(strategies: StrategyAggregator[]): void

    Register on the fly, some StrategyAggregators in LayeredTree Mediator.

    • Parameters:

      • strategies: StrategyStorage Array of StrategyAggregators instances.
    • Returns: void

    ℹ️ It's possible to register a strategyAggregator on the fly, but it's not recommended, for two reasons:

    • Because it can be confusable and dangerous for consumer at to have access or not access to a strategy at different step of application workflow.
    • And because of typescript static typing, strategyAggregator added on the fly would not be inferred.

StrategyAggregator:

Contributor

Class StrategyAggregator<SS> extends LayeredTreeComponent

The aggregator of strategies. It will store all strategies and associate those to a name.

Type parameters

  • SS: StrategyStorage

    Optionnal

Constructors:

new StrategyAggregator(name: string, strategies: SS): StrategyAggregator

Parameters

  • name: string The name of aggregator. Should be unique.

  • strategies: The object that will associate strategies to a name.

    SS| StrategyStorage (primitive: object)

    Represented by: { name: Strategy }

    ℹ️ check available strategies

Returns: StrategyAggregator

Methods:

  • isRegistered

    isRegistered(): boolean

    Check if the StrategyAggregator have been registered in LayeredTree.

    • Returns: boolean
  • processStrategy

    processStrategy(strategyName: string): unknown

    To process all StrategyLayer ( methods ) in this aggregator.

    • Parameters:

      • strategyName: string

        The name associate to the strategy to pick.

    • Returns: unknown

      The strategy picked return.


LayeredTreePlugin:

Consumer

Class LayeredTreePlugin<LT> extends LayeredTreeComponent

LayeredTreePlugin is a plugin that allow access to all registered strategies to a LayeredTree instance (mediator).

Instantiation Generic type should be type with the typeof LayeredTree instance. It is intended to have a good type inference.

const plugin = new LayeredTreePlugin<typeof layeredTreeInstance>(...)

It should be register in a LayeredTree instance (mediator).

Type parameters

  • LT: LayeredTree

    Optionnal

    typeof a LayeredTree instance. (It couldn't be inferred, so it should be defined at the instantiation.)

Constructors:

new LayeredTreePlugin(name: string): LayeredTreePlugin

Parameters

  • name: string The name of plugin. Should be unique.

Returns: LayeredTreePlugin

Methods:

  • isRegistered

    isRegistered(): boolean

    Check if the component have been registered in LayeredTree.

    • Returns: boolean
  • strategySubscribe

    strategySubscribe(strategyName: string): (Anonymous function): unknown

    info

    • Parameters:

      • strategyName: string

        The strategy name to subscribe.

    • Returns: (Anonymous function): unknown

      An Anonymous function that take a StrategyLayer method to attach at the subscribed strategy registered. The return of this function depend of strategy definition.


Strategies references:


Injection

Contributor

Class Injection<Return, Data> extends Strategy

Injection Strategy is usually to inject some data at a moment of an application flow. It will process the stored InjectionMethod (function) and return the data generated by this one. Each subscription override the precedent one.

Type parameters

  • Return: unknown | undefined

    Optionnal

    The return type waited for the InjectionMethod. (It couldn't be inferred, so it should be defined at the instantiation.)

  • Data: any

    Optionnal * depend if some data is needed

    To pass some data to the InjectionMethod, which is needed to make its job. (It couldn't be inferred, so it should be defined at the instantiation.)

Constructors:

new Injection<Return, Data>(defaultReturnValue: Return | undefined): Injection

Parameters

  • defaultReturnValue: Return | undefined Optionnal

    To force a default return value to the injection. bu default it's undefined

    default: undefined

Returns: Injection

Methods:

  • use

    use(injectionMethod: InjectionMethod<Return, Data>): void

    To use the injection Method for this Injection strategy, concretely it fill or replace injectionMethod property.

    • Parameters:

      • injectionMethod: InjectionMethod<Return, Data>

        A simple method that could take some data in parameter or not, it depend of the need. and return Return type defined on instantiation.

        • InjectionMethod<R, D> = (() => R) | ((data: D) => R)
    • Returns: void

      The return type that have been defined on instantiation.

  • process

    process(data: Data): Return | process(): Return

    Process the injectionMethod. It should not run manually. It's executed by the StrategyAggregator through the processStrategy method.

    • Parameters:

      • data: Data

        Optionnal

        The data parameter of the InjectionMethod. Not needed if not define on instantiation

    • Returns: Return

      The return type that have been defined on instantiation.


Pipeline

Contributor

Class Pipeline<Source, Data>

Pipeline strategy is usually used to modify some data source at a moment of an application flow. It will process a stack of registered middleware (function) and return an augmented/altered provided source.

Type parameters

  • Source: any

    Optionnal

    The source to pass at the entry of the pipeline. (It couldn't be inferred, so it should be defined at the instantiation.)

  • Data: any

    Optionnal * depend if some data is needed

    To pass some data to the Middleware, which is needed to make its job. (It couldn't be inferred, so it should be defined at the instantiation.)

Constructors:

new Pipeline<Source, Data>(): Pipeline

Returns: Pipeline

Methods:

  • use

    use(middleware: Middleware<Source, Data>): this

    To use a middleware for this pipeline, concretely it add a middleware to the middleware stack.

    • Parameters:

      • middleware: Middleware<Source, Data> Middleware to use. A middleware can be Simple or Complex:

        • SimpleMiddleware<Source>: It's a function that take in parameter the src and the next function.

            (src: Source, next) => next(src)

          Next function, in the case of simple middleware, is a function that take the src in parameter (to transfer it to the next middleware), and return the value of the next middleware processed.

          ⚠️ The important thing to understand is if you don't call next function it break the chain of middlewares process.

          ⚠️ In the case you don't call next function, the process function will stop the chain to this middleware and the pipeline will return the value returned by the current middleware.

          for example:

            (src) => src

          The value returned by the middleware is the result of the pipeline processing.

    - `ComplexMiddleware<Source, Data>`: It's a function that take in parameter the src, data object and the  next function.
        ```ts
          (src: Source, data: Data, next) => next(src, data)
        ```
  
        Next function, in the case of complex middleware, is a function that take the src,, and a data object (to transfer its to the next middleware) in parameter. 
        
        It return the value of the next middleware processed.
  
        ⚠️ The important thing to understand is if you don't call next function it break the
          chain of middlewares process.
  
        ⚠️ In the case you don't call next function, the process function will stop the chain to this middleware and the pipeline will return the value returned by the current middleware.

        For example: 

        ```ts
          (src, data) => src
        ```

        **The value returned by the middleware is the result of the pipeline processing.**

- ***Returns***: `this` 

  return the same `use` function to easily chain middleware assignation.
  • process

    process(src: Source, data: Data): Source | process((src: Source): Source

    Process the Middleware stack.

    • Parameters:

      • src: Source

        The source transferred through the middleware stack.

      • data: Data

        Optionnal

        The data to transfer to the middleware stack.

    • Returns: Source

      Return the same type of the original source.


Secondary references:


Strategy

Contributor

Class Strategy

Abstract

Strategy is an abstract class. It should be extended to create a strategy able to be mapped in a StrategyAggregator.

ℹ️ It's useful to define a custom strategy.

Type parameters

Depend of the strategy definition.

Constructors:

Depend of the strategy definition.

Returns: Strategy

Methods:

  • use

    use(method: StrategyLayer): any

    Tell to the strategy to use a method StrategyLayer. Basically StrategyLayer is a method. The signature of this method depend of the strategy.

    (For example in Pipeline strategy the expected method is a Middleware)

    • Parameters:

      • method: StrategyLayer

        The method to add on the strategy stack.

    • Returns: any

      Depend of the strategy definition.


StrategyObserver

Library

Class StrategyObserver<SS>

The StrategyObserver is used by the Mediator to manage all strategies registered, subscription, and execution.

ℹ️ Actually it only for deep contribution. It's documented for information, you should not use it, as consumer of the layered-tree solution.

Type parameters

  • SS: StrategyStorage

Constructors:

new StrategyObserver(): StrategyObserver

Returns: StrategyObserver

Methods:

  • addBaseObserver

    addBaseObserver(strategyObserver: StrategyObserver<StrategyStorage>): void

    To register all observable strategies from an other observer to the current one.

    • Parameters:

      • strategyObserver: StrategyObserver<StrategyStorage>

        The strategyObserver to register.

    • Returns: void

  • register

    register(strategyName: keyof SS | string, strategy: Strategy): void

    Register a strategy to the current observer.

    ℹ️ It's not advisable to directly register strategy from the observer, usually it's done by the LayeredTree mediator.

    • Parameters:

      • strategyName: keyof SS | string

        The name associate to the strategy

      • strategy: Strategy

        The strategy to Register

    • Returns: void

  • subscribe

    subscribe(strategyName: keyof SS | string, method: (...args: any[]) => any): void

    Subscribe to a strategy registered in the current observer.

    • Parameters:

      • strategyName: keyof SS | string

        The name associate to the strategy to pick.

      • method: (...args: any[]) => any

        The method to add on the strategy stack.

        Depend of the strategy picked.

    • Returns: any

      Depend of the strategy picked.

  • fire

    fire(strategyName: keyof SS | string): Strategy["process"]

    Fire the strategy processing.

    ℹ️ It's not advisable to directly call fire function from the observer, usually it's done by the LayeredTree mediator or StrategyAggregator through processStrategy method.

    • Parameters:

      • strategyName: keyof SS | string

        The name associate to the strategy.

    • Returns: Strategy["process"]

      It return the process function of the picked Strategy.


LayeredTreeComponent

Library

Class LayeredTreeComponent<LT>

Abstract

LayeredTreeComponent is an abstract class that should be extended to all components that could be registered in LayeredTree mediator, to make them communicate together.

ℹ️ Actually it only for deep contribution. It's documented for information, you should not use it, as consumer of the layered-tree solution.

Type parameters

Constructors:

new LayeredTreeComponent(name: string): LayeredTreeComponent

Parameters

  • name: string The name of the component. Should be unique.

Returns: LayeredTreeComponent

Accessors

  • layeredTree

    get layeredTree(): undefined | LT
    set layeredTree(layeredTree: undefined | LT): void

    The LayeredTree instance (mediator) associate to this component when it's registered.

    • Returns: undefined | LT
  • name

    get name(): symbol
    set name(name: symbol): void

    Get the unique name.

    • Returns: symbol

Methods:

  • isRegistered

    isRegistered(): boolean

    Check if the component have been registered in LayeredTree.

    • Returns: boolean

Tools


LayeredTreeToStrategyLayer

Type alias LayeredTreeToStrategyLayer<<LT, SN>

Very useful Utility Type to infer a specific StrategyLayer ( method ) signature from a LayerTree instance type. When StrategyLayer are abstracted for example, it could be very handy.

// With LayeredTreeToStrategyLayer utility Type, actions, data, next the return function are well inferred.
const addRobBankAction: LayeredTreeToLayerTreeToStrategyLayer<GameLayeredTree, 'character.initial.randomInitialActions'> = (
    actions,
    data,
    next,
) => {
    return next(
        {
            ...
        },
        data,
    );
};

characterPlugin.strategySubscribe('character.initial.randomInitialActions')(addRobBankAction);

ℹ️ For full example check here

Type parameters

  • LT: LayeredTree The typeof LayeredTree instance used to extract the desired StrategyLayer.
  • SN: string The strategy name.

Methods:

  • isRegistered

    isRegistered(): boolean

    Check if the component have been registered in LayeredTree.

    • Returns: boolean

Injection Strategy:

🤝 Contributing

Contributions, issues and feature requests are welcome!

Feel free to check issues page. You can also take a look at the contributing guide.

👤 Main Contributors

Nicolas Ribes Github: @easyni Author*

💚 Show your support

Give a ⭐️ if this project helped or interest you !

📝 License

This project is MIT licensed.