UNPKG

mutable-store

Version:

a mutable state management library for javascript

169 lines (149 loc) 4.04 kB
export function getProps(obj: Record<string, any>) { const props = new Set(); const builtins = [ Object.prototype, Array.prototype, Function.prototype, String.prototype, Number.prototype, Boolean.prototype, Symbol.prototype, Date.prototype, RegExp.prototype, Map.prototype, Set.prototype, WeakMap.prototype, WeakSet.prototype, Promise.prototype, Error.prototype, ]; while (obj && !builtins.includes(obj)) { for (const key of Reflect.ownKeys(obj)) { if (key !== "constructor") { props.add(key); } } obj = Object.getPrototypeOf(obj); } return Array.from(props); } function makePropsReadOnly<T extends Record<string, any>>( obj: T, keys: (keyof T)[] ) { for (const key of keys) { if (obj.hasOwnProperty(key)) { Object.defineProperty(obj, key, { writable: false, configurable: false, enumerable: true, // or false depending on your needs }); } else { console.warn(`Property "${String(key)}" does not exist on the object.`); } } return obj; } export type TMutableStore<T> = T & { subscribe: (fn: () => void) => () => void; ___thisIsAMutableStore___: true; ___version___: number; }; export default function createMutableStore<T extends Record<string, any>>( mutableState: T ): TMutableStore<T> { if (mutableState === null || typeof mutableState !== 'object') { throw new Error("mutableState must be an object"); } const props: (keyof T)[] = getProps(mutableState) as (keyof T)[]; if (props.includes("subscribe")) { throw Error( "subscribe is a reserved keyword for the store, please use another name for your property" ); } makePropsReadOnly( mutableState, props.filter( (item) => typeof mutableState[item] === "function" || mutableState[item]?.___thisIsAMutableStore___ ) ); const subscriptions = new Set<() => void>(); const internalStoreUnSubs = new Set<() => void>(); const getInternalStores = () => props .filter((item) => mutableState[item].___thisIsAMutableStore___) .map((item) => mutableState[item]); const subscribe = (fn: () => void) => { subscriptions.add(fn); return () => { subscriptions.delete(fn); }; }; function callSubs() { try { subscriptions.forEach((fn) => fn()); } catch (e) { console.error(e); } } // auto subscribe to internal stores function autoSubscribeToInternalStores() { getInternalStores().forEach((item) => internalStoreUnSubs.add( item.subscribe(() => { callSubs(); }) ) ); } autoSubscribeToInternalStores(); props.forEach((prop) => { if ( typeof mutableState[prop] === "function" && prop.toString().startsWith("set_") ) { const originalMethod = mutableState[prop]; // Replace the method with our wrapped version Object.defineProperty(mutableState, prop, { value: function (this: T, ...args: any[]) { const result = originalMethod.apply(this, args); setTimeout(() => { callSubs(); autoSubscribeToInternalStores(); }, 0); return result; }, writable: false, configurable: false, enumerable: true }); } }); // Add our internal properties first Object.defineProperties(mutableState, { subscribe: { value: subscribe, writable: false, configurable: false, enumerable: false }, ___thisIsAMutableStore___: { value: true, writable: false, configurable: false, enumerable: false }, ___version___: { value: 1, writable: false, configurable: false, enumerable: false } }); // Now make the object non-extensible after all modifications Object.preventExtensions(mutableState); return mutableState as unknown as TMutableStore<T>; } export const Store = createMutableStore;