@layered-tree/core v1.0.0-dev.0
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
oryarn start:core:example:restricted
at root
📚 API References
Main references:
LayeredTree:
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
LTO:
LayeredTreeOption<SA, LT>
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.
- type:
layeredTrees:
- type:
LT
|LayeredTree[]
Array of LayeredTree instances.
- type:
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:
- plugins:
LayeredTreePlugin<any>[]
Array of LayeredTreePlugin.
- plugins:
Returns:
void
registerStrategies
registerStrategies(strategies: StrategyAggregator[]): void
Register on the fly, some StrategyAggregators in LayeredTree Mediator.
Parameters:
- strategies:
StrategyStorage
Array of StrategyAggregators instances.
- strategies:
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:
Class StrategyAggregator<SS>
extends LayeredTreeComponent
The aggregator of strategies. It will store all strategies and associate those to a name.
Type parameters
SS:
StrategyStorage
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
- Returns:
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:
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
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
- Returns:
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
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
The return type waited for the InjectionMethod. (It couldn't be inferred, so it should be defined at the instantiation.)
Data:
any
* 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
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 theprocessStrategy
method.Parameters:
data:
Data
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
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
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
* 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 beSimple
orComplex
: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
The data to transfer to the middleware stack.
Returns:
Source
Return the same type of the original source.
Secondary references:
Strategy
Class Strategy
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
. BasicallyStrategyLayer
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
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
Class LayeredTreeComponent<LT>
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
- LT: LayeredTree
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
- Returns:
name
get name(): symbol set name(name: symbol): void
Get the unique name.
- Returns:
symbol
- Returns:
Methods:
isRegistered
isRegistered(): boolean
Check if the component have been registered in LayeredTree.
- Returns:
boolean
- Returns:
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
- Returns:
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.
4 years ago