import cloneDeep from 'lodash/cloneDeep'; /** * Creates a reactive state object that batches changes and notifies all subscribers with the full old and new state. * * @param initialState - The initial state object * @returns Object with { state: reactive proxy, subscribe: function to add callbacks } * * @example * ```typescript * const { state, subscribe } = createBatchedReactiveState({ count: 0, user: { name: 'Alice' } }); * * const unsubscribe1 = subscribe((newState, oldState) => { * console.log('Subscriber 1:', oldState, '->', newState); * }); * * const unsubscribe2 = subscribe((newState, oldState) => { * console.log('Subscriber 2:', newState.count); * }); * * state.count = 1; // Triggers both subscribers once * state.user.name = 'Bob'; // Triggers both subscribers once (batched) * * unsubscribe1(); // Remove first subscriber * state.count = 2; // Only subscriber 2 is called * ``` */ export function createReactiveState(initialState: T) { const callbacks = new Set<(newState: T, oldState: T) => void>(); let isBatching = false; let oldState: T | null = null; let scheduled = false; const rootState = cloneDeep(initialState); function createReactiveObject(obj: any): any { return new Proxy(obj, { get(target, property, receiver) { const value = Reflect.get(target, property, receiver); return typeof value === 'object' && value !== null ? createReactiveObject(value) : value; }, set(target, property, value, receiver) { if (!isBatching) { isBatching = true; oldState = cloneDeep(rootState); } const success = Reflect.set(target, property, value, receiver); if (success) { if (!scheduled) { scheduled = true; queueMicrotask(() => { callbacks.forEach((cb) => cb(rootState, oldState!)); isBatching = false; oldState = null; scheduled = false; }); } } return success; }, }); } const state = createReactiveObject(rootState); return [ state, function subscribe(callback: (newState: T, oldState: T) => void) { callbacks.add(callback); return () => callbacks.delete(callback); }, ] as const; }