@2toad/reflex
Version:
A simple approach to state management
268 lines • 9.33 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.map = map;
exports.filter = filter;
exports.merge = merge;
exports.combine = combine;
exports.scan = scan;
exports.debounce = debounce;
exports.switchMap = switchMap;
exports.mergeMap = mergeMap;
exports.concatMap = concatMap;
exports.catchError = catchError;
const reflex_1 = require("./reflex");
/**
* Maps values from a source reflex using a transform function
* @param source The source reflex to map from
* @param fn The transform function to apply to each value
*/
function map(source, fn) {
const errorState = (0, reflex_1.reflex)({ initialValue: null });
const mapped = (0, reflex_1.reflex)({ initialValue: fn(source.value) });
mapped._errorState = errorState;
source.subscribe((value) => {
try {
mapped.setValue(fn(value));
// Clear error state on success
errorState.setValue(null);
}
catch (error) {
errorState.setValue(error);
}
});
return mapped;
}
/**
* Filters values from a source reflex using a predicate function
* @param source The source reflex to filter
* @param predicate The function to test each value
*/
function filter(source, predicate) {
const filtered = (0, reflex_1.reflex)({
initialValue: predicate(source.value) ? source.value : undefined,
});
source.subscribe((value) => predicate(value) && filtered.setValue(value));
return filtered;
}
/**
* Merges multiple reflex sources into a single reflex that emits whenever any source emits
* @param sources Array of reflex sources to merge
*/
function merge(sources) {
if (!sources.length) {
throw new Error("merge requires at least one source");
}
const [firstSource, ...restSources] = sources;
const merged = (0, reflex_1.reflex)({ initialValue: firstSource.value });
const subscriber = (value) => merged.setValue(value);
restSources.forEach((source) => source.subscribe(subscriber));
firstSource.subscribe(subscriber);
return merged;
}
/**
* Combines multiple reflex sources into a single reflex that emits arrays of their latest values
* @param sources Array of reflex sources to combine
*/
function combine(sources) {
if (!sources.length) {
throw new Error("combine requires at least one source");
}
const combined = (0, reflex_1.reflex)({
initialValue: sources.map((source) => source.value),
});
sources.forEach((source, index) => {
source.subscribe((value) => {
combined.setValue([...combined.value.slice(0, index), value, ...combined.value.slice(index + 1)]);
});
});
return combined;
}
/**
* Creates a reflex that accumulates values over time using a reducer function
* @param source The source reflex
* @param reducer The reducer function to accumulate values
* @param seed The initial accumulator value
*/
function scan(source, reducer, seed) {
const scanned = (0, reflex_1.reflex)({ initialValue: seed });
let accumulator = seed;
source.subscribe((value) => {
accumulator = reducer(accumulator, value);
scanned.setValue(accumulator);
});
return scanned;
}
/**
* Debounces a reflex source, only emitting after a specified delay has passed without any new emissions
* @param source The source reflex to debounce
* @param delayMs The delay in milliseconds
*/
function debounce(source, delayMs) {
const debounced = (0, reflex_1.reflex)({ initialValue: source.value });
let timeoutId = null;
source.subscribe((value) => {
if (timeoutId) {
clearTimeout(timeoutId);
}
timeoutId = setTimeout(() => debounced.setValue(value), delayMs);
});
return debounced;
}
/**
* Projects each value from the source to an inner reflex, cancelling previous projections
* when a new value arrives. Like map, but for async/reflex operations where only the latest matters.
* @param source The source reflex
* @param project Function that returns a reflex or promise for each source value
*/
function switchMap(source, project) {
const result = (0, reflex_1.reflex)({ initialValue: undefined });
let currentProjection = Symbol();
const handleProjection = async (value, projectionId) => {
try {
const projected = project(value);
if (projected instanceof Promise) {
const resolvedValue = await projected;
if (currentProjection === projectionId) {
result.setValue(resolvedValue);
}
}
else {
if (currentProjection === projectionId) {
result.setValue(projected.value);
projected.subscribe((newValue) => {
if (currentProjection === projectionId) {
result.setValue(newValue);
}
});
}
}
}
catch (error) {
console.error("[Reflex SwitchMap Error]", error);
throw error;
}
};
handleProjection(source.value, currentProjection);
source.subscribe((value) => {
currentProjection = Symbol();
handleProjection(value, currentProjection);
});
return result;
}
/**
* Projects each value from the source to an inner reflex, maintaining all active projections
* concurrently. Like map, but for async/reflex operations that should run in parallel.
* @param source The source reflex
* @param project Function that returns a reflex or promise for each source value
*/
function mergeMap(source, project) {
const result = (0, reflex_1.reflex)({ initialValue: undefined });
const handleProjection = async (value) => {
try {
const projected = project(value);
if (projected instanceof Promise) {
result.setValue(await projected);
}
else {
result.setValue(projected.value);
projected.subscribe((newValue) => result.setValue(newValue));
}
}
catch (error) {
console.error("[Reflex MergeMap Error]", error);
throw error;
}
};
handleProjection(source.value);
source.subscribe(handleProjection);
return result;
}
/**
* Projects each value from the source to an inner reflex, processing projections in sequence.
* Like map, but for async/reflex operations that must complete in order.
* @param source The source reflex
* @param project Function that returns a reflex or promise for each source value
*/
function concatMap(source, project) {
const result = (0, reflex_1.reflex)({ initialValue: undefined });
const queue = [source.value];
let isProcessing = false;
const processQueue = async () => {
if (isProcessing || !queue.length)
return;
isProcessing = true;
const value = queue[0];
try {
const projected = project(value);
if (projected instanceof Promise) {
result.setValue(await projected);
}
else {
result.setValue(projected.value);
const unsubscribe = projected.subscribe((newValue) => result.setValue(newValue));
await new Promise((resolve) => setTimeout(resolve, 0));
unsubscribe();
}
queue.shift();
}
catch (error) {
console.error("[Reflex ConcatMap Error]", error);
queue.shift();
}
finally {
isProcessing = false;
processQueue();
}
};
processQueue();
source.subscribe((value) => {
queue.push(value);
processQueue();
});
return result;
}
/**
* Catches errors in a reflex stream and allows for recovery or fallback values
* @param source The source reflex
* @param errorHandler Function that returns a reflex or value to recover from the error
*/
function catchError(source, errorHandler) {
const result = (0, reflex_1.reflex)({ initialValue: undefined });
let currentFallback = null;
const handleError = (error) => {
currentFallback?.unsubscribe();
currentFallback = null;
const handled = errorHandler(error);
if (handled instanceof Object && "subscribe" in handled) {
const fallbackReflex = handled;
const unsubscribe = fallbackReflex.subscribe((value) => result.setValue(value));
currentFallback = { reflex: fallbackReflex, unsubscribe };
result.setValue(fallbackReflex.value);
}
else {
result.setValue(handled);
}
};
try {
result.setValue(source.value);
}
catch (error) {
handleError(error);
}
source.subscribe((value) => {
if (currentFallback) {
currentFallback.unsubscribe();
currentFallback = null;
}
try {
result.setValue(value);
}
catch (error) {
handleError(error);
}
});
const errorState = source._errorState;
errorState?.subscribe((error) => error && handleError(error));
return result;
}
//# sourceMappingURL=operators.js.map