@2toad/reflex
Version:
A simple approach to state management
283 lines • 10.4 kB
JavaScript
;
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