export interface Change { path: string; type: "add" | "update" | "delete"; value?: any; functionName?: string; fileName?: string; id: string; from: "react" | "update" | "set"; } let listenToGet = false; let storedUsed: Set> = new Set(); export const setListenToGet = (value: boolean) => { listenToGet = value; }; export const getUsedStores = () => { return storedUsed; }; export const setUsedStores = (value: Set>) => { storedUsed = value; }; export const addUsedStore = (value: GetSet) => { storedUsed.add(value); }; export interface GetSetBubble { getState(): T; subscribe(callback: (changes: Change[]) => void): void; } export type BubbleReturnType = { [K in keyof T]: T[K] extends GetSetBubble ? U : T[K]; }; export type BubbleType = { getState: () => BubbleReturnType; subscribe: Function; reset: () => void; }; export type ValType = ( newStateFn?: T | ((currentState: T) => void) | undefined, versionName?: string ) => T; export type UpdateType = ( updateFn: (currentState: T) => void, saveCurrentStateAs?: string ) => T; export type SetType = ( newStateFn: T | ((currentState: T) => T), saveCurrentStateAs?: string ) => T; export type GetSet< T extends string | number | Object | null | undefined, S extends Object = {} > = { ___25304743758287906getset: boolean; get(): T; getState(): T; actions?: S | undefined; getSavedVersion: (versionName: string) => T; saveVersion: (versionName?: string) => void; reset: () => void; // deepClone: () => T; val: ValType; // reset: () => void; // resetIfNoListeners: () => void; set(newStateFn: T | ((currentState: T) => T), saveCurrentStateAs?: string): T; update( updateFn: (currentState: T) => void, saveCurrentStateAs?: string ): T; react( fn: (state: T, changes?: Change[]) => void, dependencyArrayFn: (state: T) => any[] ): void; subscribe(callback: (changes: Change[], type: string) => void): () => void; // dispatch: (setter: (...args:any[]) => any, ...args: any[]) => T; }; type Func = (...args: T) => R; export function debounce( func: Func, delay: number ): Func { let timerId: ReturnType; return function (this: any, ...args: T) { const context = this; clearTimeout(timerId); timerId = setTimeout(() => { func.apply(context, args); }, delay); }; } export function getset< T extends number | string | boolean | object | null | undefined, S extends object = {} >( initialState: T, actions?: (getterSetter: { val: ValType; get: () => T; set: SetType; update: UpdateType; }) => S, config: { deepFreeze: boolean; supportSettersAndInheritance: boolean; } = { deepFreeze: true, supportSettersAndInheritance: true, } ): ValType & GetSet { let versions: Record = {}; let state1: T = initialState; let state2 = typeof initialState === "object" ? createStateProxy(state1) : state1; const subscriptions = new Set<{ fn: Function; type: string }>(); const reactions = new Set(); let accumulatedChanges = new Map(); let alreadyDestructuredPaths: string[] = []; let setInProgress = false; let subscriptionCallInProgress = false; let stopCallingSubscriptions = false; let reactionInProgress = false; let functionName = ""; let fileName = ""; if (config.supportSettersAndInheritance) { //@ts-ignore const methodProps = typeof state1 === "object" && state1 !== null && !Array.isArray(state1) ? getAllMethodProps(state1) : []; if (methodProps.length > 0) { const allProps = getAllProps(state1); state1 = {} as T; allProps.forEach((prop) => { //@ts-ignore const temp = initialState[prop]; //@ts-ignore if (typeof temp === "function") { //@ts-ignore state1[prop as keyof T] = function (...args: any[]) { return prop.startsWith("_") || prop.startsWith("action_") ? temp.call(state1, ...args) : dispatch(temp, ...args); }; } else { //@ts-ignore state1[prop as keyof T] = temp; } }); } } config.deepFreeze && recursiveDeepFreeze(state1); initialState = state1; function clearAccumulatedChanges() { accumulatedChanges.clear(); alreadyDestructuredPaths = []; } function updateValue( obj: any, path: string, val: any, isDelete: boolean = false ): void { if (typeof obj !== "object" || obj === null) { return; } let pathAccumulated = ""; const keys = path.split("."); let currentObj = obj; for (let i = 0; i < keys.length - 1; i++) { const key = keys[i]; pathAccumulated = pathAccumulated.length > 0 ? pathAccumulated + "." + key : key; if (!alreadyDestructuredPaths.includes(pathAccumulated)) { if ( currentObj.hasOwnProperty(key) && typeof currentObj[key] === "object" ) { if (Array.isArray(currentObj[key])) { currentObj[key] = [...currentObj[key]]; } else { currentObj[key] = { ...currentObj[key] }; } alreadyDestructuredPaths.push(pathAccumulated); currentObj = currentObj[key]; } else { // If the key doesn't exist or is not an object, create an empty object currentObj[key] = {}; currentObj = currentObj[key]; } } else { currentObj = currentObj[key]; } } //@ts-ignore if (isDelete) { delete currentObj[keys[keys.length - 1]]; } else { currentObj[keys[keys.length - 1]] = val; } } function createStateProxy(obj: any, path = "", setterName?: string): T { return new Proxy(obj, { get(_target, key: string) { const fullPath = path ? `${path}.${key}` : key; //@ts-ignore let obj = fullPath.split(".").reduce((a, b) => a[b], state1); if (typeof obj === "object" && obj !== null) { //@ts-ignore return createStateProxy( config.deepFreeze ? Array.isArray(obj) ? [...obj] : { ...obj }: obj, path ? `${path}.${key}` : key, setterName ); } else { return obj; } }, set(target, key: string, value) { const fullPath = path ? `${path}.${key}` : key; const oldValue = target[key]; updateValue(state1, fullPath, value); const change: Change = { path: fullPath, from: reactionInProgress ? "react" : "update", type: oldValue === undefined ? "add" : "update", value, functionName: setterName || functionName, fileName: setterName ? "Setter" : fileName, id: Math.random().toString(), }; addChangeToList(change); return true; }, deleteProperty(target, key: string) { const fullPath = path ? `${path}.${key}` : key; const oldValue = target[key]; updateValue(state1, fullPath, undefined, true); delete target[key]; const change: Change = { path: fullPath, functionName, fileName, from: reactionInProgress ? "react" : "update", type: "delete", value: oldValue, id: Math.random().toString(), }; addChangeToList(change); return true; }, }); } function addChangeToList(change: Change) { accumulatedChanges.set(change.path, change); } let reactionCallInProgress = false; const callReactions = () => { reactionCallInProgress = true; const changes = [...accumulatedChanges.values()].reverse(); for (const reaction of reactions) { try { !stopCallingSubscriptions && reaction(changes); } catch (e) { console.error(e); } } reactionCallInProgress = false; }; const callSubs = debounce(() => { config.deepFreeze && recursiveDeepFreeze(state1); subscriptionCallInProgress = true; const changes = [...accumulatedChanges.values()].reverse(); for (const subscription of [...subscriptions]) { !stopCallingSubscriptions && subscription.fn(changes, subscription.type); } stopCallingSubscriptions = false; clearAccumulatedChanges(); subscriptionCallInProgress = false; }, 0); function get() { if (listenToGet) { addUsedStore(val); } return state1; } function set( newState: T | ((currentState: T) => T), saveCurrentStateAs: string = "" ): T { checkIfOtherCallInProgress(); if (!reactionInProgress) { getCallerInfo(); } monitorUpdates(); setInProgress = true; const newValue = typeof newState === "function" ? newState(state1) : newState; state1 = newValue; state2 = typeof state1 === "object" ? createStateProxy(state1, "") : state1; addChangeToList({ from: "set", functionName, fileName, type: "update", path: "", id: Math.random().toString(), value: newValue, }); saveCurrentStateAs && (versions[saveCurrentStateAs] = state1); setInProgress = false; callReactions(); callSubs(); // updateState1(true); functionName = ""; fileName = ""; return state1; } function checkIfOtherCallInProgress() { if (reactionInProgress || reactionCallInProgress) { throw Error( "update/set calls are not allowed in reactions. instead directly change the state" ); } if (setInProgress) { throw Error( "update/set calls are not allowed from within the call backs of update/set" ); } if (subscriptionCallInProgress) { throw Error( "update/set calls are not allowed from the subscriptions to prevent memory leaks. Use Reactions Instead" ); } } function getCallerInfo(stackIndex = 3) { try { const error = new Error(); const stackLines = error?.stack?.split?.("\n"); const temp = stackLines?.[stackIndex]?.trim?.()?.split?.(" "); const a = temp?.[temp.length > 2 ? 2 : 1]?.split("?")?.[0]?.split?.("/"); const b = temp?.length || 0 > 2 ? temp?.[1] : ""; (functionName = (temp?.length || 0) < 3 ? "unknown" : b || ""), (fileName = a?.[a?.length - 1] || ""); } catch (error: any) {} } let dispatchInProgress = false; function preUpdateAndDispatch() { monitorUpdates(); checkIfOtherCallInProgress(); getCallerInfo(); setInProgress = true; } function dispatch(setter: (...args: any[]) => any, ...args: any[]): T { if (!dispatchInProgress) { try { dispatchInProgress = true; preUpdateAndDispatch(); updateOrDispatch(setter, undefined, true, ...args); dispatchInProgress = false; } catch (e) { console.error(e); } finally { dispatchInProgress = false; } return state1; } else { setter.call(state2, ...args); return state1; } } function updateOrDispatch( setter: (...args: any[]) => any, saveCurrentStateAs?: string, fromDispatch: boolean = false, ...args: any[] ) { try { if (typeof setter === "function" && typeof state1 === "object") { const oldState = state1; state1 = shallowClone(state1); state2 = createStateProxy(state1, "", setter.name); fromDispatch ? setter.call(state2, ...args) : setter(state2, saveCurrentStateAs); setInProgress = false; alreadyDestructuredPaths = []; if ([...accumulatedChanges].length === 0) { state1 = oldState; } else { callReactions(); callSubs(); } } else { throw Error( "update function cant be used for primitive states. you can use either set or val but pass updated values instead of function to update state" ); } } catch (e) { console.error(e); } finally { fileName = ""; functionName = ""; setInProgress = false; } } function update( newState: (currentState: T) => void, saveCurrentStateAs?: string ): T { preUpdateAndDispatch(); updateOrDispatch(newState, saveCurrentStateAs, false); return state1; } function subscribe( callback: (changes: Change[], type: string) => void, type = "" ) { let obj = { fn: callback, type }; subscriptions.add(obj); return () => { subscriptions.delete(obj); }; } let callCount = 0; let lastCalledTimestamp = Date.now(); function monitorUpdates() { callCount++; let now = Date.now(); if (now - lastCalledTimestamp < 1000) { // Check if 1 second has elapsed if (callCount > 100) { callCount = 0; lastCalledTimestamp = now; stopCallingSubscriptions = true; throw Error( "update / set functions are called more than 100 times in 1 second, check if this is because you are changing the state that's being the dependency of the reaction." ); } // Reset the counter and timestamp for the next interval } else { callCount = 0; lastCalledTimestamp = now; } } function react( fn: (State: T, changes?: Change[]) => void, dependencyArrayFn: (state: T) => any[] ) { getCallerInfo(3); let a = functionName; let b = fileName; functionName = ""; fileName = ""; let oldRefs: any = dependencyArrayFn(get()); reactions.add((changes: Change[]) => { functionName = a; fileName = b; reactionInProgress = true; const newRefs: any = dependencyArrayFn(get()); try { if (checkIfDepsRefsChanged(oldRefs, newRefs)) { oldRefs = newRefs; state2 = createStateProxy(state1); fn(state2, changes); } } catch (e) { console.error("error occured in reaction", e); } finally { functionName = ""; fileName = ""; reactionInProgress = false; } }); } function checkIfDepsRefsChanged(oldRef: any[], newRef: any[]) { if (oldRef.length !== newRef.length) { return true; } for (let i = 0; i < oldRef.length; i++) { if (oldRef[i] !== newRef[i]) { return true; } } return false; } const _actions = actions && typeof actions === "function" ? actions({ get, set, update, val }) : undefined; if (_actions && Object.keys(_actions).length > 0) { Object.keys(_actions).forEach((key) => { //@ts-ignore if (typeof _actions[key] === "function") { //@ts-ignore _actions[key] = _actions[key].bind(_actions); } }); } function val( newStateFn?: T | ((currentState: T) => void) | undefined, versionName?: string ): T { if (arguments.length === 0) { return get(); } else { if (typeof newStateFn === "function") { return update(newStateFn as (currentState: T) => void, versionName); } else if (arguments.length > 0) { //@ts-ignore return set(newStateFn, versionName); } } return get(); } val.___25304743758287906getset = true; val.get = get; val.getState = get; val.react = react; // deepClone: () => (structuredClone || deepClone)(get()), val.reset = () => { set(initialState); }; val.set = set; val.update = update; val.saveVersion = (name?: string) => { if (name) { versions[name] = state1; } }; val.val = val; val.getSavedVersion = (name: string) => { return versions[name]; }; val.actions = _actions; val.subscribe = subscribe; // dispatch, return val; } export function i( initialState: T, setterFn: (currentState: T) => void, deepFreeze: boolean = false ): T { const returnVal = getset(initialState, undefined, { deepFreeze: false, supportSettersAndInheritance: false, }).update(setterFn); if(deepFreeze){ recursiveDeepFreeze(returnVal); } return returnVal; } export const create = getset; export const createState = getset; function shallowClone(obj: T): T { return Array.isArray(obj) ? ([...obj] as T) : typeof obj === "object" ? { ...obj } : obj; } export function getAllProps(instance: any): any[] { const props: string[] = Object.keys(instance); let currentPrototype = Object.getPrototypeOf(instance); while (currentPrototype !== null && currentPrototype !== Object.prototype) { props.push(...Object.getOwnPropertyNames(currentPrototype)); currentPrototype = Object.getPrototypeOf(currentPrototype); } const unique = [...(new Set(props)?.values?.() || [])].filter( (item) => item !== "constructor" ); return unique; } function getAllStates(state: T): BubbleReturnType { const props = getAllProps(state); const newState: any = {}; props.forEach((prop: keyof T) => { newState[prop] = state[prop]; //@ts-ignore if ( //@ts-ignore state[prop].getState && //@ts-ignore state[prop].___25304743758287906getset ) { //@ts-ignore newState[prop] = state[prop].getState(); } else if (typeof state[prop] === "function") { //@ts-ignore newState[prop] = state[prop].bind(state); } }); return newState; } function subscribeToAll(fn: Function, state: T): Function { const props = getAllProps(state); const unsubs: any = []; const unsubscribe = () => { unsubs.forEach((i: Function) => i()); }; props.forEach((prop: keyof T) => { //@ts-ignore if ( state[prop] && typeof state[prop] !== "function" && //@ts-ignore state[prop].getState ) { //@ts-ignore unsubs.push(state[prop].subscribe(fn, prop)); } }); return unsubscribe; } export function createBubble>( state: T ): { getState: () => BubbleReturnType; subscribe: (fn: Function) => Function; get: () => BubbleReturnType; reset: () => void; set: ( newStateOrFunction: | Partial> | (( currentState: Partial> ) => Partial>) ) => BubbleReturnType; } { let s = getAllStates(state); const fnToSubscribe = () => { s = getAllStates(state); }; const reset = () => { Object.keys(state).forEach((key) => { //@ts-ignore state[key].reset?.(); }); }; // subscribeToAll(fnToSubscribe, state); const bubble = { ...state, getState() { if (listenToGet) { addUsedStore(bubble as any); } return s; }, reset, set( newStateOrFunction: | Partial> | (( currentState: Partial> ) => Partial>) ) { let obj = typeof newStateOrFunction === "function" ? newStateOrFunction(s) : newStateOrFunction; Object.keys(obj).forEach((key) => { //@ts-ignore state[key].set(obj[key]); }); fnToSubscribe(); return this.getState(); }, get() { return this.getState(); }, subscribe: (fn: Function) => subscribeToAll((changes: Change[] = [], type: string = "") => { fnToSubscribe(); fn( changes.map((item) => ({ ...item, path: type + "." + item.path, })) ); }, state), }; return bubble; } // lets you export function createUpdator(state: { getState: () => T; subscribe: any; actions?: any; setState?: any; reset?: any; }) { return (updatorFn: (currentState: T) => void) => { const s = create(state.getState()); state.setState?.(s.update(updatorFn)); return s.get(); }; } export type MemoReturnType = { getState: () => T; subscribe: (fn: () => void) => () => void; value: T; get: () => T; }; export function createMemo( fn: () => T, deps: () => any[] = () => [] ): MemoReturnType { let returnVal: T = null as any; let subs: any[] = []; let unsubs: any[] = []; let oldDeps = deps(); const run = () => { setUsedStores(new Set()); unsubs.forEach((i) => i()); setListenToGet(true); try { returnVal = fn(); } catch (e) { console.error("error occured in memo", e); } finally { setListenToGet(false); } storeVal.value = returnVal; getUsedStores().forEach((store) => { unsubs.push( store.subscribe((_changes: Change[]) => { if (checkIfDepsChanged(oldDeps, deps()) || deps().length === 0) { run(); oldDeps = deps(); subs.forEach((i) => i()); } }) ); }); }; const get = () => { if (listenToGet) { addUsedStore(storeVal as any); } return returnVal; }; var storeVal = { getState: get, get: get, subscribe: (fn: Function) => { subs.push(fn); return () => { subs = subs.filter((i) => i !== fn); }; }, value: returnVal, }; run(); return storeVal; } function checkIfDepsChanged(oldDeps: any[], newDeps: any[]) { if (oldDeps.length !== newDeps.length) return true; for (let i = 0; i < oldDeps.length; i++) { if (oldDeps[i] !== newDeps[i]) return true; } return false; } export function createEffect(fn: () => void, deps: () => any[] = () => []) { let unsubs: any[] = []; let oldDeps = deps(); const run = () => { setUsedStores(new Set()); unsubs.forEach((i) => i()); setListenToGet(true); try { fn(); } catch (e) { console.error("error occured in effect", e); } finally { setListenToGet(false); } getUsedStores().forEach((store) => { unsubs.push( store.subscribe((_changes: Change[]) => { setTimeout(() => { if (checkIfDepsChanged(oldDeps, deps())) { run(); oldDeps = deps(); } }, 0); }) ); }); }; run(); } export function recursiveDeepFreeze(obj: any): T { Object.keys(obj as any).forEach((key) => { if ( typeof obj[key] === "object" && obj[key] !== null && !Object.isFrozen(obj[key]) ) { recursiveDeepFreeze(obj[key]); } }); return Object.freeze(obj); } // function getAllNonMethodProps(obj: any): string[] { // // Traverse prototype chain to get all non-method properties // return getAllProps(obj).filter((key) => typeof obj[key] !== "function"); // } function getAllMethodProps(obj: any): string[] { // Traverse prototype chain to get all method properties return getAllProps(obj).filter((key) => typeof obj[key] === "function"); } export const atom = getset; export const molecule = createBubble;