0.13.10 • Published 2 years ago

yassi v0.13.10

Weekly downloads
25
License
MIT
Repository
github
Last release
2 years ago

yassi

Yet Another Simple Store Implementation

Overview

Yassi is a very simple javascript store implementation.
Yassi keeps your store lean and mean while allowing data extensions via facades.
Yassi store's properties have unique single owner thus reduce code complexity and maintenance.

While there are many stores out there storing your application state well enough such as redux, flux and others, they are all too cumbersome and opinionated.
Using these libraries requires too much boilerplate.
It is even recommended by top influential developers, such as Dan Abramov on his post You might not need redux, as well as by Redux FAQ page to use these libraries only when your state become complex.

As Pete Hunt, one of the early contributors to React, says:

Similarly, Dan Abramov, one of the creators of Redux, says:

In addition, redux, flux and others provide public access to all their properties from anyone that can access the store.
In many cases and especially when the project grows, the result of such broad access behaviour is the manipulation of single application state's properties from multiple locations which increase code complexity and maintenance hassle.

Yassi approach is different.
It is so simple that you will want and should use it from your first line of code!!!
It is publicly readable but privately writable which means everybody can access the property but only the owner can change it

Yassi key features are: 1. Unopinionated store - you don't need reducers or actions. Just mark the property you want to store with @yassit and you good to go 1. Don't want annotation? No problem, use the non annotated version of Yassi's operators. 1. Publicly readable - all properties in the store can be accessed by any consumer. 1. Privately writeable - only the owner of the property (i.e the class or object own the property) can apply changes to the property. 1. Lean and mean - with facades you may create store entries that cannot be changed directly and only changes as a reaction to store changes. This behaviour allowing you to keep the store as lean as possible. 1. Reactive store as well as not - You may get a property's value from the store using @select or use @observe to get a reactive observable on the given property 1. When needed, you may commiunicate with a property's owner via endpoints to execute some exported actions on the owner's properties 1. You can register any middleware to Yassi's operators, allowing you to create powerfull tools on top of Yassi.
The following middleware are available:

  • beforeStore
  • afterStore
  • beforeRetrieve
  • afterRetrieve

Check the Todo MVC example for Yassi with Angular: TodoMVC Yassi

Installation

npm install --save yassi

Usage

  • Import Yassi
  • Declare the properties that you like to store by add the @yassit('propertyNameInStore') before the declaration of the property or use it without annotation as follow - yassit('propertyNameInStore', ownerObject, ownerPropertyName)
  • On another class, create a property and declare it with either @select or @observe to read the property from the store.
import {yassit} from 'yassi';

class MyCoolClass {
  @yassit('srcNumProp1')
  numProp1: number;
  
  @yassit('srcNumProp2')
  numProp2: number = 2;
}

That's it, These properties are stored on class instantiation!!! They are also publicly readable but privately writable which mean only instances of MyCoolClass can change them but everyone can read them
Let's see how to use them in other component

import {select} from 'yassi';

class AnotherComponent {
  prop1: string;
  
  @select('srcNumProp1')
  propFromStore: number;
}

Again, that's it!!! Any change to MyCoolClass.numProp1 will reflect on AnotherComponent.propFromStore. Note that AnotherComponent.propFromStore is a read only property and you cannot change it here, only via MyCoolClass

Want reactive??! Use @observe instead of @select

import {observe} from 'yassi';

class AnotherComponent {
  prop1: string;
  
  @observe('srcNumProp1') 
  propFromStore: Observable<number>;
}

Now any change to MyCoolClass.numProp1 will reflect reactivly on AnotherComponent.propFromStore

Introducing Facades

One of the key concept of Yassi is to make the store lean and simple. To do so I created Yassi's Facades.

The idea of facades is to encourage users to use the store (via yassit) only for core data items and any derived properties should come in the form of Facades.

In Yassi, properties are stored by the owner thus only the owner object may change their values. Facades are stored properties that does not have an owner and therefore cannot change directly (to be precise, they have owner but it is privately owned by Yassi).

Instead Facades listen to changes on one or more stored properties and triggers a function provided by the user on that changes to create a new result from these stored properties

Let's see how Facades works:

Assume we have the following class with its yassit:

import {yassit} from 'yassi';

class ServerUserInfo {
  @yassit('firstName')
  firstName: string = 'John';
  
  @yassit('lastName')
  lastName: string = 'Doe';

  @yassit('birthDate')
  birthDate: number = 946677600000; 
}

If we want a full representation of the user info we should create facade instead of creating a new property owned by some object that can be use everywhere

import {yassi} from 'yassi';

yassi.facade('userInfo', ['firstName', 'lastName', 'birthDate'], ([fName, lName, bDate]) => {
  return {
    first: fName,
    last: lName,
    full: `${fName} ${lName}`,
    birth: new Date(bDate),
  }
});

Now the store has an entry userInfo which will hold the entire user info object but the beauty is that this entry is not changeable by anyone, it can only change as a reaction to changes on the source properties it was declared on!!!

An example usage of that facade will be similar to any other stored property

import {observe} from 'yassi';

class AnotherComponent {  
  @observe('userInfo') 
  myUserInfo: Observable<object>;

  printUserInfo() {
    this.myUserInfo.subscribe((userInfo: object) => {
      console.log(JSON.stringify(userInfo));
    })
  }
}

Any change to one of the properties in the store firstName, lastName or birthDate will trigger a print of the new version of userInfo

The return value from a facade will continue in the facade chain to any listener declared using select or observe.
To prevent facades of firing undesired results, a user may prevent the facade from sending its values to the next handler in the chain by returning an object contains the breakFacadeChain operator and the payload operator as follow:

{
  breakFacadeChain: boolean,
  payload: any
}

If the breakFacadeChain is true, the chain will stop and the facade result will not continue to its listeners.
If the breakFacadeChain is missing (null or undefined), the entire return value will be sent to the listeners.
If the breakFacadeChain is false, the chain will continue and the facade will send the value of payload to the listeners.

Communicating with property's owner

As mentioned before, one of the key concept of Yassi is publicly readable/privately writable properties which mean that only the property's owner may alter the property.
There are cases where one would like to change or request a change from outside (i.e. not from the owner) to a property that owned by other object.
In this case Yassi introduce yassi.castRequest (replacing the deprecated yassi.communicate) which allows anyone in the system to cast a request to the owner in some manner.
How this request will be handled and what it will do is still in the control of the property's owner thus keeping the key concept of privately writable intact.

How it is done

Declare endpoint function in the owner class/object with @endpoint.
Note that the @endpoint functions must be declared after @yassit for the store to recognize it.
Then call for castRequest() with the owner's property name (this is how Yassi recognize the owner you want to cast request to), the endpoint name and zero or more arguments that will passed to endpoint.

Example:

import {yassit, endpoint} from 'yassi';

class ServerUserInfo {
  @yassit('firstName')
  firstName: string = 'John';
  
  @yassit('lastName')
  lastName: string = 'Doe';

  @yassit('birthDate')
  birthDate: number = 946677600000;
  
  @yassit('userType')
  userType: string = 'visitor'; // Not an enum, I know. It is still an example only

  @endpoint()
  changeUserType(type: string, requester: any) {
    if (!authorizedUsers.get(requester)) {
      return;
    }
    if(type !== 'visitor' && type !== 'user') {
      return;
    }
    this.userType = type;
  }
}

And somewhere in the code you can request the endpoint from another component:

import {yassi} from yassi;

function someFunc() {
  // Do many things or not
  yassi.castRequest('userType', 'changeUserType', 'user', currentUser);  
}

The castRequest function takes the following arguments:
1. The owner's stored property name in which we like to cast request to (note it could be any other name declared on the owner such as firstName/lastName/birthDate).
1. The name of the owner's endpoint that we like to execute meaning the name of the function declared by @endpoint.
1. Zero or more arguments the endpoint function expect as input.

Note that the control of what is bean done is still at the hand of the owner.

Republishing properties to all its listeners

There might be cases which a property is changed deep in the object hierarchy such as an array of objects where one of the objects' property was changed, since this property is not tracked by Yassi, we will not update the listeners for this change.
In such cases you may use republish(property name in store) to publish the property again to all the listerns.
Example:

// Somewhere in the code we have:
@yassit('itemList')
itemList = [{active: true}];

// And a listener somewhere else
@observe('itemList')
itemListFromOutside: Observable<any>;

// and later on the code do:
itemList[0].active = false;

// in order to see this change on `itemListFromOutside` we need to tell yassi to publish the list again as follow:
yassi.republish('itemList');

API

  • @yassit(name: string) - prefixed on a class's property that you like to add it's values to the store upon instantiation
  • yassi.yassit(name: string, owner?: any, name?: string) - without annotation the owner and name are object and it's property in correspond that we like to store
  • @select(name: string) - prefixed on a class's property when you want to get a store value of named property
  • yassi.select(name: string, targetObj: object, targetProp: string) - without annotation, the targetObj and targetProp are object and it's property in correspond that we like to apply the store data on.
  • @observe(name: string) - prefixed on a class's property when you want to observe a store propety via observable. You should subscribe to that observable to get any change in value.
  • yassi.observe(name: string, targetObj: object, targetProp: string) - without annotation, the targetObj and targetProp are object and it's property in correspond that we like to apply the store data on reactively.
  • facade(name: string, yassiElementsName: string[], fn: (yassiElementsValue: any[]) => any) - The facade results will be stored in the store under the name entry and will execute the fn on each change on one of the stored values represented by yassiElementsName
  • registerMiddleware(action: string, position: string, fn: (proto, key, val) => void = null) - Register a middleware function that will execute on the target action (either before or after it). Good place to execute loggers or monitoring tools.
  • castRequest(yassiPropName: string, endpointName: string, ...functionParams) - execute an endpoint of name endpointName of the owner of yassiPropName with the given parameters.
  • communicate(yassiPropName: string, endpointName: string, functionParams: any[]) - execute an endpoint of name endpointName of the owner of yassiPropName with the given parameters.
    Note this method is deprecated and was replaced with the castRequest which use rest parameters instead of array for the endpoint

Middlewares

You can register middleware functions that will be triggered synchronously before/after the yassi decorator apply

  • You can register the default middleware (i.e. print action to console) by simply call registerMiddleware without callback. Example:
import {registerMiddleware} from 'yassi';

registerMiddleware('yassit', 'before');
// Instantiation of MyCoolClass as well as updates to its properties will print the properties to the console.
const myClass = new MyCoolClass();
  • You may provide a call back function to registerMiddleware that will execute every time the decorator is run Example:
import {registerMiddleware} from 'yassi';

registerMiddleware('yassit', 'after',
    (proto: any, key: string, val: any) => console.log(`-------${proto.constructor.name}.${key}=${val}-------`));
// Instantiation of MyCoolClass will trigger the given callback calls to the console.
const myClass = new MyCoolClass();

You can register any amount of middlewares for yassit, select and observe before and/or after it.

To Do

  1. Make @yassi work on different instances of a class. Right now it support only one instance of a class
  2. Run benchmark against known stores
  3. Add more examples
  4. Add UI tools/extensions
0.13.10

2 years ago

0.13.6

2 years ago

0.13.8

2 years ago

0.13.4

2 years ago

0.13.5

2 years ago

0.13.2

3 years ago

0.13.1

3 years ago

0.12.2

3 years ago

0.12.1

3 years ago

0.11.0

4 years ago

0.10.3

4 years ago

0.10.1

4 years ago

0.9.6

4 years ago

0.9.4

4 years ago

0.9.2

4 years ago

0.9.1

4 years ago

0.8.1

4 years ago

0.7.6

4 years ago

0.7.4

4 years ago

0.7.5

5 years ago

0.7.3

5 years ago