@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:kidoryarn start:core:example:restrictedat 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>): LayeredTreeParameters
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): unknownTo process all StrategyLayer ( methods ) registered LayeredTree instance.
Parameters:
strategyName:
stringThe name associate to the strategy to pick.
Returns:
unknownThe strategy picked's return.
registerPlugins
registerPlugins(plugins: LayeredTreePlugin<any>[]): voidRegister some LayeredTreePlugin in LayeredTree Mediator.
Parameters:
- plugins:
LayeredTreePlugin<any>[]Array of LayeredTreePlugin.
- plugins:
Returns:
void
registerStrategies
registerStrategies(strategies: StrategyAggregator[]): voidRegister on the fly, some StrategyAggregators in LayeredTree Mediator.
Parameters:
- strategies:
StrategyStorageArray 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): StrategyAggregatorParameters
name:
stringThe 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(): booleanCheck if the StrategyAggregator have been registered in LayeredTree.
- Returns:
boolean
- Returns:
processStrategy
processStrategy(strategyName: string): unknownTo process all StrategyLayer ( methods ) in this aggregator.
Parameters:
strategyName:
stringThe name associate to the strategy to pick.
Returns:
unknownThe 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:
LayeredTreetypeof a LayeredTree instance. (It couldn't be inferred, so it should be defined at the instantiation.)
Constructors:
new LayeredTreePlugin(name: string): LayeredTreePluginParameters
- name:
stringThe name of plugin. Should be unique.
Returns: LayeredTreePlugin
Methods:
isRegistered
isRegistered(): booleanCheck if the component have been registered in LayeredTree.
- Returns:
boolean
- Returns:
strategySubscribe
strategySubscribe(strategyName: string): (Anonymous function): unknowninfo
Parameters:
strategyName:
stringThe strategy name to subscribe.
Returns:
(Anonymous function): unknownAn Anonymous function that take a StrategyLayer method to attach at the subscribed strategy registered. The
returnof 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 | undefinedThe 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): InjectionParameters
defaultReturnValue:
Return | undefinedTo force a default return value to the injection. bu default it's
undefineddefault:
undefined
Returns: Injection
Methods:
use
use(injectionMethod: InjectionMethod<Return, Data>): voidTo 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
Returntype defined on instantiation.InjectionMethod<R, D> = (() => R) | ((data: D) => R)
Returns:
voidThe return type that have been defined on instantiation.
process
process(data: Data): Return | process(): ReturnProcess the injectionMethod. It should not run manually. It's executed by the
StrategyAggregatorthrough theprocessStrategymethod.Parameters:
data:
DataThe data parameter of the InjectionMethod. Not needed if not define on instantiation
Returns:
ReturnThe 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:
anyThe 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>(): PipelineReturns: Pipeline
Methods:
use
use(middleware: Middleware<Source, Data>): thisTo 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 beSimpleorComplex: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) => srcThe 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): SourceProcess the Middleware stack.
Parameters:
src:
SourceThe source transferred through the middleware stack.
data:
DataThe data to transfer to the middleware stack.
Returns:
SourceReturn 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): anyTell to the strategy to use a method
StrategyLayer. BasicallyStrategyLayeris a method. The signature of this method depend of the strategy.(For example in Pipeline strategy the expected method is a Middleware)
Parameters:
method:
StrategyLayerThe method to add on the strategy stack.
Returns:
anyDepend 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(): StrategyObserverReturns: StrategyObserver
Methods:
addBaseObserver
addBaseObserver(strategyObserver: StrategyObserver<StrategyStorage>): voidTo 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): voidRegister 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|stringThe name associate to the strategy
strategy:
StrategyThe strategy to Register
Returns:
void
subscribe
subscribe(strategyName: keyof SS | string, method: (...args: any[]) => any): voidSubscribe to a strategy registered in the current observer.
Parameters:
strategyName:
keyof SS|stringThe name associate to the strategy to pick.
method:
(...args: any[]) => anyThe method to add on the strategy stack.
Depend of the strategy picked.
Returns:
anyDepend 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
processStrategymethod.Parameters:
strategyName:
keyof SS|stringThe 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): LayeredTreeComponentParameters
- name:
stringThe name of the component. Should be unique.
Returns: LayeredTreeComponent
Accessors
layeredTree
get layeredTree(): undefined | LT set layeredTree(layeredTree: undefined | LT): voidThe LayeredTree instance (mediator) associate to this component when it's registered.
- Returns:
undefined | LT
- Returns:
name
get name(): symbol set name(name: symbol): voidGet the unique name.
- Returns:
symbol
- Returns:
Methods:
isRegistered
isRegistered(): booleanCheck 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:
stringThe strategy name.
Methods:
isRegistered
isRegistered(): booleanCheck 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.
5 years ago