primitate
Version:
Managing your app state by two methods
585 lines (493 loc) • 16.5 kB
text/typescript
type Dictionary = { [key :string]: any }
// *************
// utilities
// *****************************************************
function identity<T>(x: T) { return x; }
const toString = Object.prototype.toString;
function isObj(obj: any) { return toString.call(obj) === "[object Object]"; }
function isArray(arr: any) { return toString.call(arr) === "[object Array]"; }
function isPrimitive(x: any) { return x != null && typeof x !== "object"; }
function throwNullLike(x: any) {
if (x == null)
throw new TypeError("Primitate cannot include null or undefined");
}
function throwObjType(x: any) {
if (typeof x === "object" && !(isObj(x) || isArray(x)))
throw new TypeError("Primitate cannot include typeof 'object' except [object Object] and [object Array]");
}
function throwTypeDiff(a: any, b: any) {
if(typeof a !== typeof b)
throw new TypeError(
["Primitate not allow changing the state structure."
,`Your value '${b}' must be typeof '${typeof a}'`].join(" "));
}
// function forEach<T>(arr: T[], fun: (arg: T) => void) {
// let i = 0;
// while(i < arr.length)
// }
function once<T extends Function>(fun: T): T {
let emitted = false, result: any;
return function() {
if (emitted) return result;
emitted = true;
return result = fun.apply(null, arguments);
} as any as T
}
function spliceOne(list: any[], index: number) {
for (let i = index, j = i + 1, n = list.length; j < n; i += 1, j += 1)
list[i] = list[j]
list.pop();
}
function isEqualDeep(a: any, b: any) {
if (a === b)
return true;
const isP = isPrimitive;
if (a !== b && isP(a))
return false;
const toString = Object.prototype.toString;
let stack = [a, b];
while(stack.length) {
const c_b = stack.pop();
const c_a = stack.pop();
switch (toString.call(c_a)) {
case "[object Object]":
for (let key in c_a) {
const v_c_a = c_a[key];
const v_c_b = c_b[key];
if (v_c_a === v_c_b)
continue;
if (isP(v_c_a))
return false;
stack[stack.length] = v_c_a;
stack[stack.length] = v_c_b;
}
break;
case "[object Array]":
if (c_a.length !== c_b.length)
return false;
let i = -1;
while(++i < c_a.length) {
const v_c_a = c_a[i];
const v_c_b = c_b[i];
if (v_c_a === v_c_b)
continue;
if (isP(v_c_a))
return false;
stack[stack.length] = v_c_a;
stack[stack.length] = v_c_b;
}
break;
default:
if (c_a !== c_b)
return false;
}
}
return true;
}
function cloneFreezeDeep<T>(Target_Initial: any, Target: any): T {
throwNullLike(Target);
throwObjType(Target);
switch(toString.call(Target)) {
case "[object Object]":
return cloneFreezeObjectDeep<T>(Target_Initial, Target);
case "[object Array]":
return cloneFreezeArrayDeep<T>(Target_Initial, Target);
default:
return checkPrimitiveValue<T>(Target_Initial, Target);
}
}
function checkPrimitiveValue<T>(Target_Initial: any, Target: any): T {
throwNullLike(Target);
throwObjType(Target);
throwTypeDiff(Target_Initial, Target);
return Target;
}
function cloneFreezeArrayDeep<T>(Target_Initial: any, Target: any): T {
throwNullLike(Target);
throwObjType(Target);
const isFrozen = Object.isFrozen;
if (typeof Target === "object" && isFrozen(Target))
return Target;
if (Target.length === 0)
return Object.freeze([]) as any;
const Result: any[] = new Array(Target.length);
const Stack = [Result, Target_Initial, Target];
while(Stack.length) {
const t = Stack.pop()!;
const _t_ini: any = Stack.pop();
const t_ini: any = _t_ini.length === 0 ? t[0] : _t_ini[0];
const r = Stack.pop()!;
throwNullLike(t);
let i = -1;
while(++i < t.length) {
const t_current = t[i];
throwNullLike(t_current);
if (typeof t_current === "object" && isFrozen(t_current)) {
r[i] = t_current;
continue;
}
switch(toString.call(t_current)) {
case "[object Object]":
r[i] = cloneFreezeObjectDeep(t_ini, t_current);
break;
case "[object Array]":
throwTypeDiff(t_ini, t_current);
const newR = new Array(t_current.length);
r[i] = newR;
const stack_l = Stack.length;
Stack[stack_l] = newR;
Stack[stack_l + 1] = t_ini;
Stack[stack_l + 2] = t_current;
break;
default:
throwObjType(t_current);
throwTypeDiff(t_ini, t_current);
r[i] = t_current;
}
}
Object.freeze(r);
}
return Result as any;
}
function cloneFreezeObjectDeep<T>(InitialTarget: any, Target: any): T {
throwNullLike(Target);
throwObjType(Target);
const result = <T>{};
const stack: any[] = [result, InitialTarget, Target];
const isFrozen = Object.isFrozen;
while(stack.length) {
const t = stack.pop()!;
const it = stack.pop();
const r = stack.pop()!;
if (it)
for (let key in it)
if (!t.hasOwnProperty(key))
throw new TypeError(`Cannot change the state structure. You lack key '${key}'`);
for (let key in t) {
const t_current = t[key];
throwNullLike(t_current);
if (it && !it.hasOwnProperty(key))
throw new TypeError(`Cannot change the state structure. You have an extra key '${key}'`);
const t_isObjType = typeof t_current === "object";
if (t_isObjType && isFrozen(t_current))
continue;
switch (toString.call(t_current)) {
case "[object Object]":
const newR = {};
r[key] = newR;
const stack_l = stack.length;
stack[stack_l] = newR;
stack[stack_l + 1] = it ? it[key] : undefined;
stack[stack_l + 2] = t_current;
break;
case "[object Array]":
r[key] = cloneFreezeArrayDeep(t_current, it[key]);
break;
default:
throwObjType(t_current);
throwTypeDiff(it[key], t_current);
r[key] = t_current;
}
}
Object.freeze(r);
}
return result;
}
function mergeDeep<T extends Dictionary>(source: T, paths: string[], target: any): T {
const result = <T>{}
const max = paths.length - 1;
const path_last = paths[0];
let i = paths.length;
let stack_r: Dictionary = result;
let stack_s: Dictionary = source;
if (max === 0) {
for (let key in stack_s) {
stack_r[key] = stack_s[key];
}
} else {
while(--i) {
const k = paths[i]
const newS: Dictionary = stack_s[k]
const newR: Dictionary = {};
for (let key in stack_s) {
stack_r[key] = stack_s[key];
}
for (let key in newS) {
newR[key] = newS[key];
}
stack_r[k] = newR;
Object.freeze(stack_r);
stack_r = newR;
stack_s = newS;
}
}
stack_r[path_last] = target;
Object.freeze(stack_r);
return result;
}
// *************
// Primitate
// *****************************************************
type Pick<T, U> = (state: T) => U
// Action
export type Action<Next, Target> = (next?: Next) => Target
export type ActionSource<State, Target, NextValue>
= (prevState: Target, next: NextValue | undefined, initialState: Target, stateTree: State) => Target
interface ActionTools<State, Target> {
pick: (State: State) => Target
, ActState_Initial: Target
, convActResultToState: (ActResult: Target) => State
, cloneFreezeDeepActResult: (ActState_Initial: Target, ActResult: Target) => Target
}
// Listener
type Listener<State> = (state: State) => void
interface ListenerItem<State> {
Listener: Listener<State>
, isLazy: boolean
, TimerID: number | undefined
}
type Subscribe<State>
= (...picks: ((state: State) => any)[])
=> ( listener: (err: Error | string | null, state: State) => void )
=> () => void
const Key_ObjectPath = "__PriOP";
const Key_Listener = "__PriL";
const Key_ID_Timer_Initial = "__PriTI";
interface TreeItem<State> {
__PriOP: string[]
__PriL: ListenerItem<State>[][]
}
interface Tree<State> {
[key: string]: Tree<State> | TreeItem<State>
}
class PrimitateTree<State> {
private _tree: Tree<State>
private _isEmitting = false;
private _Stack_removeListener: (() => void)[] = [];
constructor(State_Initial: State, private getCurrentState: () => State) {
const Key_L = Key_Listener;
const Key_O = Key_ObjectPath;
const Key_I = Key_ID_Timer_Initial;
if (isArray(State_Initial) || isPrimitive(State_Initial)) {
this._tree = { [Key_L]: [[]], [Key_O]: [], [Key_I]: undefined } as any;
return;
}
const Result = {};
const Stack: any[] = [Result, State_Initial];
const Stack_Source_Roots: any[] = []; // [key, listeners]
let Stack_L_Children: any[] = [];
let Stack_L_Parents: any[] = [];
let Stack_Key: string[] = [];
while (Stack.length) {
// s: current source
// r: current result
// k: current key
const s = Stack.pop()
const r = Stack.pop()
const k = Stack.pop();
const l = Stack.pop();
if (!isObj(s))
continue;
if (k) {
if (isObj(s)) {
Stack_Key.push(k);
Stack_L_Parents.push(l);
} else {
Stack_Key.pop();
Stack_L_Parents.pop();
}
// Came back to the root of the source
if (Stack_Source_Roots[Stack_Source_Roots.length - 2] === k) {
Stack_L_Children = [Stack_Source_Roots.pop()!]; // [[[]]]
Stack_L_Parents = Stack_L_Children[0].concat();
Stack_Key = [Stack_Source_Roots.pop()!];
}
}
const Stack_L_C_Length = Stack_L_Children.length;
const Keys_CurrentDepth = Stack_Key.concat();
for (let key in s) {
const newS = s[key];
const newR: Dictionary = {};
const newLs = [[]].concat(Stack_L_Parents);
newR[Key_L] = newLs
newR[Key_O] = Keys_CurrentDepth.concat(key).reverse();
newR[Key_I] = undefined;
r[key] = newR;
const s_isObj = isObj(newS);
if (s_isObj)
Stack_L_Children.push(newLs);
for (let i = 0; i < Stack_L_C_Length; i++) {
Stack_L_Children[i].push(newLs[0]);
}
if (!r.hasOwnProperty(Key_L)) {
Stack_Source_Roots.push(key);
Stack_Source_Roots.push(newLs);
}
Stack.push(newLs[0]);
Stack.push(key);
Stack.push(newR);
Stack.push(newS);
}
}
this._tree = Result;
}
public getObjectPath(pick: Pick<Tree<State>, TreeItem<State>>): string[] {
return pick(this._tree).__PriOP;
}
public addListener(Listener: Listener<State>, isLazy: boolean, pickers: Pick<Tree<State>, TreeItem<State>>[]) {
const ListenerItem_New: ListenerItem<State> = { Listener, isLazy, TimerID: undefined };
let i = 0;
const _tree = this._tree;
while (i < pickers.length) {
const item = pickers[i++](_tree);
(item.__PriL)[0].push(ListenerItem_New);
}
return once(() => {
if (this._isEmitting)
this._Stack_removeListener.push(() => this.removeListener(pickers, ListenerItem_New));
else
this.removeListener(pickers, ListenerItem_New);
});
}
public removeListener(pickers: Pick<Tree<State>, TreeItem<State>>[], listener: ListenerItem<State>) {
let i = -1;
while (++i < pickers.length) {
const listeners = pickers[i](this._tree).__PriL[0];
spliceOne(listeners, listeners.indexOf(listener));
}
}
private _doRemoveListeners() {
const Stack_removeListener = this._Stack_removeListener;
let i = 0;
while(Stack_removeListener.length > 0)
Stack_removeListener.pop()!();
}
public emitListener(pick: Pick<Tree<State>, TreeItem<State>>): void {
this._isEmitting = true;
const { __PriL } = pick(this._tree);
const Listeners_Count_Length =
__PriL.reduce( (m, arr) => m + arr.length , 0);
let Listeners_Count_Current = 0;
const getCurrentState = this.getCurrentState;
const State_Current = getCurrentState();
let i = 0
while (i < __PriL.length) {
const Listeners = __PriL[i++];
let j = 0;
while (j < Listeners.length) {
const Listener = Listeners[j++];
Listeners_Count_Current++;
if (Listener.isLazy) {
if (Listener.TimerID) clearTimeout(Listener.TimerID);
if (Listeners_Count_Current === Listeners_Count_Length)
Listener.TimerID = setTimeout( () => {
Listener.Listener(getCurrentState());
this._doRemoveListeners();
}, 0);
else
Listener.TimerID = setTimeout( () => Listener.Listener(getCurrentState()), 0);
} else
Listener.Listener(State_Current);
}
}
if (Listeners_Count_Current === Listeners_Count_Length)
this._doRemoveListeners();
}
}
export class PrimitateClass<State> {
protected _State_Current: State
protected _State_Initial: State
protected _stateWasChanged = true
private _ActionTools = <{ [key: string] : ActionTools<State, any> }>{}
private _PrimitateTree: PrimitateTree<State>
constructor(initialState: State) {
this._State_Current = cloneFreezeDeep<State>(initialState, initialState);
this._State_Initial = this._State_Current;
this._PrimitateTree = new PrimitateTree(initialState, () => this._State_Current);
}
private _action<Target, NextValue>(Action: ActionSource<State, Target, NextValue>, ActionTools: ActionTools<State, Target>) {
let NextValue_Prev: NextValue | undefined;
const { pick, cloneFreezeDeepActResult, ActState_Initial, convActResultToState } = ActionTools;
const _tree = this._PrimitateTree;
const _getCurrentState = () => this.getCurrentState();
return (NextValue?: NextValue) => {
if (!this._stateWasChanged && NextValue_Prev !== undefined && isEqualDeep(NextValue, NextValue_Prev))
return ActionTools.pick(this._State_Current);
NextValue_Prev = NextValue;
const State_Prev = this._State_Current;
const ActState_Prev = pick(State_Prev);
const _Result = Action(ActState_Prev, NextValue, ActState_Initial, State_Prev);
if (isEqualDeep(ActState_Prev, _Result)) {
this._stateWasChanged = false;
return ActState_Prev;
}
const Result = cloneFreezeDeepActResult(ActState_Initial, _Result);
const State_Current = convActResultToState(Result);
this._State_Current = State_Current;
this._stateWasChanged = true;
_tree.emitListener(pick as any);
return Result;
}
}
/**
*
* Create a function that to change the current state.
*
* @template Target
* @template NextValue
* @param {(prev: State, next: NextValue | undefined, initialState: Target, stateTree)} actionSource
* @param {(state: State) => Target} [pick=identity]
* @returns {(next?: NextValue) => Target}
*
* @memberOf PrimitateClass
*/
public createAction<Target, NextValue>(
actionSource: ActionSource<State, Target, NextValue>
, pick: (state: State) => Target = identity) {
const Path_Object_Arr = this._PrimitateTree.getObjectPath(pick as any);
const Path_Object = Path_Object_Arr.join(".")
let ActionTools: ActionTools<State, Target>;
if ((<Object>this._ActionTools).hasOwnProperty(Path_Object)) {
ActionTools = this._ActionTools[Path_Object];
} else {
const ActState_Initial = pick(this._State_Current);
const Type_ActState = toString.call(ActState_Initial);
ActionTools =
{ pick
, ActState_Initial
, convActResultToState:
toString.call(this._State_Current) === "[object Object]"
? (Result: Target) => mergeDeep<State>(this._State_Current, Path_Object_Arr, Result)
: (Result: Target) => Result as any as State
, cloneFreezeDeepActResult:
Type_ActState === "[object Object]" ? cloneFreezeObjectDeep
: Type_ActState === "[object Array]" ? cloneFreezeArrayDeep
: checkPrimitiveValue
}
this._ActionTools[Path_Object] = ActionTools
}
return this._action(actionSource, ActionTools)
}
/**
*
* Listener will emitted when state changed.
*
* @param {(state: State) => void} listener
* @param {((state: State) =>any)[]} [pickers=[identity]]
* @param {boolean} [isLazy=true]
* @returns {() => void} unsubscribe
*
* @memberOf PrimitateClass
*/
public subscribe(
listener: (state: State) => void
, pickers: ((state: State) =>any)[] = [identity]
, isLazy = true) {
return this._PrimitateTree.addListener(listener, isLazy, pickers as any);
}
public getCurrentState() { return this._State_Current; }
public getInitialState() { return this._State_Initial; }
}
export function Primitate<State>(initialState: State) {
return new PrimitateClass(initialState);
}