UNPKG

@2toad/reflex

Version:

A simple approach to state management

283 lines 10.4 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.deepReflex = deepReflex; const reflex_1 = require("./reflex"); /** * Type guard to check if a value is a plain object or array */ const isObjectOrArray = (value) => { return typeof value === "object" && value !== null && !(value instanceof Date) && !(value instanceof RegExp); }; /** * Creates a deeply reactive value that can track changes to nested properties. * Uses Proxy to intercept property access and modifications. * * @param options - Configuration options for the deep reflex value * @param options.initialValue - The initial object value to store * @param options.equals - Optional custom equality function to determine if value has changed * @param options.deep - Whether to make nested objects deeply reactive (default: true) * @param options.onPropertyChange - Optional callback for individual property changes * @returns A reflex object with methods to get/set values and subscribe to changes */ function deepReflex(options) { const { deep = true, onPropertyChange } = options; const baseReflex = (0, reflex_1.reflex)(options); const state = { isUpdating: false, isBatching: false, batchedChanges: [], batchDepth: 0, }; const notifyPropertyChange = (path, value) => { if (onPropertyChange) { if (state.isBatching) { state.batchedChanges.push({ path, value }); } else { onPropertyChange(path, value); } } }; const updateValue = async (isAsync = false) => { if (!state.isBatching || state.batchDepth === 0) { if (isAsync) { await baseReflex.setValueAsync({ ...baseReflex.value }); } else { baseReflex.setValue({ ...baseReflex.value }); } } }; const createArrayProxy = (target, path = []) => { return new Proxy(target, { get(obj, prop) { const value = Reflect.get(obj, prop); if (typeof prop === "symbol" || !deep) { return value; } // Handle array methods that modify the array if (prop === "push" || prop === "pop" || prop === "shift" || prop === "unshift" || prop === "splice" || prop === "sort" || prop === "reverse") { return (...args) => { const result = Array.prototype[prop].apply(obj, args); if (!state.isUpdating) { state.isUpdating = true; try { notifyPropertyChange(path, obj); updateValue(); } finally { state.isUpdating = false; } } return result; }; } return isObjectOrArray(value) ? createProxy(value, [...path, prop]) : value; }, set(obj, prop, value) { if (typeof prop === "symbol") { return Reflect.set(obj, prop, value); } const oldValue = Reflect.get(obj, prop); if (Object.is(oldValue, value)) { return true; } const newValue = deep && isObjectOrArray(value) ? createProxy(value, [...path, prop]) : value; if (!Reflect.set(obj, prop, newValue)) { return false; } if (!state.isUpdating) { state.isUpdating = true; try { notifyPropertyChange([...path, prop], newValue); updateValue(); } finally { state.isUpdating = false; } } return true; }, deleteProperty(obj, prop) { if (typeof prop === "symbol" || !Reflect.has(obj, prop)) { return Reflect.deleteProperty(obj, prop); } if (!Reflect.deleteProperty(obj, prop)) { return false; } if (!state.isUpdating) { state.isUpdating = true; try { notifyPropertyChange([...path, prop], undefined); updateValue(); } finally { state.isUpdating = false; } } return true; }, }); }; const createProxy = (target, path = []) => { if (!isObjectOrArray(target)) { return target; } if (Array.isArray(target)) { return createArrayProxy(target, path); } return new Proxy(target, { get(obj, prop) { const value = Reflect.get(obj, prop); return typeof prop === "symbol" || !deep || !isObjectOrArray(value) ? value : createProxy(value, [...path, prop]); }, set(obj, prop, value) { if (typeof prop === "symbol") { return Reflect.set(obj, prop, value); } const oldValue = Reflect.get(obj, prop); if (Object.is(oldValue, value)) { return true; } const newValue = deep && isObjectOrArray(value) ? createProxy(value, [...path, prop]) : value; if (!Reflect.set(obj, prop, newValue)) { return false; } if (!state.isUpdating) { state.isUpdating = true; try { notifyPropertyChange([...path, prop], newValue); updateValue(); } finally { state.isUpdating = false; } } return true; }, deleteProperty(obj, prop) { if (typeof prop === "symbol" || !Reflect.has(obj, prop)) { return Reflect.deleteProperty(obj, prop); } if (!Reflect.deleteProperty(obj, prop)) { return false; } if (!state.isUpdating) { state.isUpdating = true; try { notifyPropertyChange([...path, prop], undefined); updateValue(); } finally { state.isUpdating = false; } } return true; }, }); }; const proxy = createProxy(options.initialValue); const processBatchedChanges = async (isAsync = false) => { if (state.batchDepth === 0) { if (onPropertyChange) { state.batchedChanges.forEach(({ path, value }) => { onPropertyChange(path, value); }); } await updateValue(isAsync); } }; return { get value() { return proxy; }, setValue(newValue) { if (!state.isUpdating) { state.isUpdating = true; try { const newProxy = createProxy(newValue); baseReflex.setValue(newProxy); } finally { state.isUpdating = false; } } }, async setValueAsync(newValue) { if (!state.isUpdating) { state.isUpdating = true; try { const newProxy = createProxy(newValue); await baseReflex.setValueAsync(newProxy); } finally { state.isUpdating = false; } } }, subscribe(callback) { return baseReflex.subscribe(callback); }, batch(updateFn) { state.batchDepth++; const wasBatching = state.isBatching; const previousChanges = state.batchedChanges; if (!wasBatching) { state.isBatching = true; state.batchedChanges = []; } try { const result = updateFn(proxy); state.batchDepth--; if (state.batchDepth === 0) { processBatchedChanges(); } return result; } finally { if (!wasBatching) { state.isBatching = false; state.batchedChanges = previousChanges; } if (state.batchDepth === 0) { state.batchedChanges = []; } } }, async batchAsync(updateFn) { state.batchDepth++; const wasBatching = state.isBatching; const previousChanges = state.batchedChanges; if (!wasBatching) { state.isBatching = true; state.batchedChanges = []; } try { const result = await Promise.resolve(updateFn(proxy)); state.batchDepth--; if (state.batchDepth === 0) { await processBatchedChanges(true); } return result; } finally { if (!wasBatching) { state.isBatching = false; state.batchedChanges = previousChanges; } if (state.batchDepth === 0) { state.batchedChanges = []; } } }, addMiddleware: baseReflex.addMiddleware, removeMiddleware: baseReflex.removeMiddleware, }; } //# sourceMappingURL=deep-reflex.js.map