1.0.0-beta.7 • Published 1 year ago

@canaan_run/canaan v1.0.0-beta.7

Weekly downloads
-
License
MIT
Repository
github
Last release
1 year ago

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

Canaan 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 command
  • type - 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.0.0-beta.7

1 year ago

1.0.0-beta.6

1 year ago

1.0.0-beta.5

1 year ago

1.0.0-beta.4

1 year ago

1.0.0-beta.3

1 year ago

1.0.0-beta.2

1 year ago

1.0.0-beta.1

1 year ago

1.0.0-beta.0

1 year ago

1.1.1

1 year ago

1.0.0

1 year ago