@canaan_run/canaan v1.0.0-beta.7
What is Canaan?
Canaan is a frontend state management solution built around Domains and Command Query Responsibility Segregation (CQRS). Each domain focuses on a specific aspect of the application state and changes are made only through commands. Queries are used to retrieve data.
Domains can also interact with each other by subscribing to specific commands. The rules engine ensures that a state change occurs only if the command passes its rules. By default, all commands pass, but custom rules can be added as needed.
When to use Canaan?
Canaan is best for enterprise frontend applications that require a rules engine and support microfronts. It's not recommended for websites and small applications unless a rules engine is needed.
Why Canaan?
- Easy maintenance with unidirectional data flow
- High-level state-application abstraction
- Scalable and framework-agnostic
- Built with proven backend and event sourcing patterns
- Minimal boilerplate required
Why CQRS ( Command Query Responsibility Segregation )
CQRS (Command Query Responsibility Segregation) is a pattern that separates the responsibilities of handling commands (i.e., write operations) and handling queries (i.e., read operations) into separate objects or components. When applied to event-driven systems, CQRS can help to ensure that the system is able to handle the high volume and velocity of events that are typically generated in such systems, while also providing a clear separation of concerns between the different components of the system. This can make it easier to scale and maintain the system over time, as well as make it more resilient to changes in the system's requirements or environment.
Thanks for ChatGPT for generating this text
CQRS & DDD (Domain-Driven Design)
DDD, is an approach to software development that emphasizes the importance of understanding and modeling the problem domain in order to create a solution that is well-aligned with the needs of the business. This includes modeling the domain using concepts like entities, value objects, aggregates, and services, as well as applying patterns like bounded contexts and anti-corruption layers to help manage complexity and maintain a clear separation of concerns between different areas of the domain.
CQRS can be used in conjunction with DDD to create a more robust and scalable system. By using DDD to model the domain and CQRS to handle the different types of operations on that domain, we can ensure that the system is able to handle the high volume and velocity of events that are typically generated in such systems, while also providing a clear separation of concerns between the different components of the system.
Thanks for ChatGPT for generating this text
Getting started
Installation
npm i -S @canaan_run/canaan
Create new Domain
Once the command is done you can start with using Canaan
import Domain from '@canaan_run/canaan';
const myTheme = new Domain('myTheme');
Create new command
The following command will be created to control the menu open/close state.
const toggleMenu = myTheme.createCommand({
name: 'toggleMenu',
description: 'This command will change the value of the menu toggle to be used later to open or close the menu',
isPublic: true,
path: 'theme.menu.open'
});
And then can called like the following
toggleMenu(false);
Create new query
const isMenuToggled = myTheme.createQuery({
name: 'isMenuToggled',
description: 'This command will get the menu status ',
isPublic: true,
path: 'theme.menu.open',
handler: (data) => {
return data === true;
}
});
And then can called like the following
isMenuToggled();
Subscribing to a query
The subscription will be called whenever the value of the query changed
myTheme.subscribe(isMenuToggled,(currentValue, oldValue) => {
console.log("The value of your query changed to ", currentValue, " from ", oldValue);
});
Listening to a command
The commands added will be considered in case the command passes the rules engine and it will call the onCommand
method. By default the commands for the same app are subscribed.
myTheme.listen(anotherCommand.commandName);
myTheme.listen('commandNameAsString');
The listen method takes a string or an array of strings, it is recommended to use the above syntax and not using the command name as a string.
myTheme.onCommand((command)=>{
const {type, payload} = command;
console.log("A new command of the name", type, " passed the rules engine with the following payload ", payload);
});
Running the rules engine
The rules engine runs when you call the function initializeRulesEngine
and give it a JSON object that can be exported from https://www.json-rule-editor.com/.
In case an event didn't pass the rules engine will trigger a command commandFailedToPass
with the context information and the value defined on the editor.
import React, { useEffect } from 'react';
import { initializeRulesEngine,} from '@canaan_run/canaan';
import rules from './public/ruels.json';
export default function Index() {
useEffect(() => {
initializeRulesEngine(rules);
}, []);
return <></>;
}
Architecture
The main components of Canaan Architecture are
- Canaan Apps, this is an where you define the app name, commands, and queries.
- Rules Engine, will take the rules and the state to decide if this command is allowed or not
- State, where we store everything including the Canaan Apps
Canaan App
Canaan App takes requires one argument name
.
Instantiating a new Canaan App will reserve the name and if you try to instantiate the same app name it will return the old instance
Commands
Commands are actions it can either change specific state path or applies no changes on the state.
To create a new command in a specific app use app.createCommand
it will return a function that takes one argument and will be passed as payload.
Creating a new command expects an object with the following attributes.
|Attribute|Type|Description | Required
| --- | --- | --- | --- |
|name|string|The name of the command and there are no restrictions on the name| true
|description|string|The description of the command and there are no restrictions on this attribute as well| true
|isPublic|boolean|If this command can be called by different commands or not| true
|path|string|the path of the state object that will hold this value, if the path ends with []
it will push to an array, and if it ends with {}
it expects the payload to have a key attribute and will store it as key=>val
|false|
Queries
Queries are used to get specific value from the state and it can be subscribed to as well.
To create a new query use app.createQuery
and it expects an object with the following attributes
|Attribute|Type|Description | Required
|---|---|---|---|
|name|string|The name of the query and there are no restrictions on the name| true
|description|string|The description of the query and there are no restrictions on this attribute as well| true
|isPublic|boolean|If this query can be called by different commands or not| true
|path|string|the path of the state object in the same app, its not allowed to create queries on different apps, BUT you can use different queries by importing them|true|
|handler|Function|If you need to process the data from the store then return it you can pass a handler function that takes the data as an argument|false|
|defaultValue|any|In case the returned value was nil -lodash isNil is used internaly- the default value will be returned |false|
Rules Engine
We are using json-rules-engine as a dependency inside Canaan and you can use it under https://www.json-rule-editor.com/. The editor allows you to add rules based on the data and the command. There are two important things here
The Context
This is the context of the command being dispatched and it does have the following values
app
- The app for this commandtype
- The command name.payload
- The value being passed with this command.time
- A unix time stamp indicates the time that this command was fired.
The data
The rules engine accesses the state of all apps and from there you can use the data
field and add path based on the value you are accessing.
json-rules-engine allows us to add a path and will parse it while running the engine.
For example, the below code will access CanaanApp then the data object , then the counter, the layout-changeTheme object, and gets the count value.
It should be less than 5 if its not it will trigger an event commandFailedToPass
including the error defined at the rules editor.
{
"fact": "data",
"operator": "greaterThanInclusive",
"value": "5",
"path": "$.CanaanApp.data.commandCounter.layout-changeTheme.count"
},
State
We are using Zustand under the hood to manage the state due to its lightweight and flexibility.
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago