@2toad/reflex
Version:
A simple approach to state management
194 lines • 6.99 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.reflex = reflex;
/**
* Creates a reactive value that can be subscribed to for changes.
*
* @param options - Configuration options for the reflex value
* @param options.initialValue - The initial value to store
* @param options.equals - Optional custom equality function to determine if value has changed
* @param options.debug - Optional debug mode to log state changes
* @param options.middleware - Optional array of middleware functions to intercept state changes
* @returns A reflex object with methods to get/set values and subscribe to changes
*/
function reflex(options) {
const subscribers = new Set();
let currentValue = options.initialValue;
let isNotifying = false;
let isBatching = false;
let skipInitialNotify = false;
let batchQueue = [];
let middleware = options.middleware || [];
const defaultEquals = (a, b) => Object.is(a, b);
const equals = options.equals || defaultEquals;
const debugLog = (message, value) => {
if (options.debug) {
console.log(`[Reflex Debug] ${message}`, value !== undefined ? value : "");
}
};
const applyMiddleware = (value) => {
let result = value;
for (const fn of middleware) {
try {
const fnResult = fn(result);
// If middleware returns a promise, warn and skip
if (fnResult instanceof Promise) {
console.warn("[Reflex Warning] Async middleware detected in sync operation");
continue;
}
result = fnResult;
}
catch (error) {
console.error("[Reflex Middleware Error]", error);
throw error;
}
}
return result;
};
const notifySubscribers = (value) => {
if (isNotifying || (isBatching && !options.notifyDuringBatch)) {
if (isBatching) {
batchQueue.push(value);
}
return;
}
isNotifying = true;
debugLog("Notifying subscribers with value:", value);
try {
subscribers.forEach((subscriber) => {
try {
const result = subscriber(value);
if (result instanceof Promise) {
result.catch((error) => {
console.error("[Reflex Subscriber Error]", error);
});
}
}
catch (error) {
console.error("[Reflex Subscriber Error]", error);
debugLog("Error in subscriber:", value);
}
});
}
finally {
isNotifying = false;
}
};
const processQueuedUpdates = () => {
if (batchQueue.length === 0)
return;
const lastValue = batchQueue[batchQueue.length - 1];
batchQueue = [];
notifySubscribers(lastValue);
};
return {
get value() {
return currentValue;
},
setValue(newValue) {
try {
const processedValue = applyMiddleware(newValue);
if (!equals(currentValue, processedValue)) {
debugLog("Setting new value:", processedValue);
currentValue = processedValue;
if (!isBatching || options.notifyDuringBatch) {
notifySubscribers(processedValue);
}
}
else {
debugLog("Value unchanged, skipping update");
}
}
catch (error) {
console.error("[Reflex SetValue Error]", error);
throw error;
}
},
subscribe(callback) {
debugLog("New subscription added");
subscribers.add(callback);
if (!skipInitialNotify) {
try {
const result = callback(currentValue);
if (result instanceof Promise) {
result.catch((error) => {
console.error("[Reflex Initial Subscribe Error]", error);
});
}
}
catch (error) {
console.error("[Reflex Initial Subscribe Error]", error);
}
}
return () => {
debugLog("Subscription removed");
subscribers.delete(callback);
};
},
batch(updateFn) {
const wasBatching = isBatching;
isBatching = true;
skipInitialNotify = true;
try {
const result = updateFn(currentValue);
if (!wasBatching) {
// Only process queue if this is the outermost batch
processQueuedUpdates();
}
return result;
}
finally {
isBatching = wasBatching;
skipInitialNotify = false;
}
},
async setValueAsync(newValue) {
try {
let processedValue = newValue;
for (const fn of middleware) {
processedValue = await Promise.resolve(fn(processedValue));
}
if (!equals(currentValue, processedValue)) {
debugLog("Setting new value:", processedValue);
currentValue = processedValue;
if (!isBatching || options.notifyDuringBatch) {
notifySubscribers(processedValue);
}
}
else {
debugLog("Value unchanged, skipping update");
}
}
catch (error) {
console.error("[Reflex SetValue Error]", error);
throw error;
}
},
async batchAsync(updateFn) {
const wasBatching = isBatching;
isBatching = true;
skipInitialNotify = true;
try {
const result = await Promise.resolve(updateFn(currentValue));
if (!wasBatching) {
// Only process queue if this is the outermost batch
processQueuedUpdates();
}
return result;
}
finally {
isBatching = wasBatching;
skipInitialNotify = false;
}
},
addMiddleware(fn) {
middleware.push(fn);
debugLog("Middleware added");
},
removeMiddleware(fn) {
middleware = middleware.filter((m) => m !== fn);
debugLog("Middleware removed");
},
};
}
//# sourceMappingURL=reflex.js.map