@2toad/reflex
Version:
A simple approach to state management
407 lines • 14.1 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.withBackpressure = withBackpressure;
exports.buffer = buffer;
exports.sample = sample;
exports.throttle = throttle;
const types_1 = require("./types");
const reflex_1 = require("./reflex");
/**
* Creates a reflex with backpressure handling capabilities.
* The returned object combines both Reflex<T> functionality and backpressure control methods.
*
* Backpressure is a mechanism to handle situations where values are being produced faster
* than they can be consumed. This function provides several strategies to handle such situations:
*
* - Drop: Simply drops values when backpressure is applied
* - Buffer: Stores values in a buffer up to a specified size
* - Sliding: Maintains a buffer of the most recent values, dropping older ones
* - Error: Throws an error when backpressure limit is exceeded
*
* Example usage:
* ```typescript
* const source = reflex({ initialValue: 0 });
* const controlled = withBackpressure(source, {
* strategy: BackpressureStrategy.Buffer,
* bufferSize: 100,
* shouldApplyBackpressure: () => customCondition
* });
*
* // Control flow with pause/resume
* controlled.pause(); // Stop processing values
* controlled.resume(); // Resume and process buffered values
* ```
*
* @param source The source reflex to add backpressure handling to
* @param options Configuration options for backpressure handling:
* - strategy: The backpressure strategy to use (Drop, Buffer, Sliding, Error)
* - bufferSize: Maximum number of values to buffer (default: 1000)
* - shouldApplyBackpressure: Optional function to determine when to apply backpressure
* @returns A Reflex that includes backpressure control methods
*/
function withBackpressure(source, options) {
const state = {
buffer: [],
isPaused: false,
valueCount: 0,
sourceUnsubscribe: null,
subscriberCount: 0,
lastEmitTime: 0,
};
const bufferSize = options.bufferSize || 1000;
const result = (0, reflex_1.reflex)({
initialValue: source.value,
});
const processBuffer = () => {
while (state.buffer.length > 0 && !state.isPaused) {
const value = state.buffer.shift();
state.valueCount--;
result.setValue(value);
}
};
// Add backpressure control methods
result.pause = () => {
state.isPaused = true;
};
result.resume = () => {
state.isPaused = false;
if (options.strategy === types_1.BackpressureStrategy.Buffer || options.strategy === types_1.BackpressureStrategy.Sliding) {
processBuffer();
}
};
result.isPaused = () => state.isPaused;
result.getBufferSize = () => state.valueCount;
const cleanup = () => {
if (state.sourceUnsubscribe) {
state.sourceUnsubscribe();
state.sourceUnsubscribe = null;
}
state.buffer.length = 0;
state.valueCount = 0;
state.isPaused = false;
};
const startBackpressure = () => {
if (!state.sourceUnsubscribe) {
state.sourceUnsubscribe = source.subscribe((value) => {
result.setValue(value);
});
}
};
// Override subscribe to manage backpressure
const originalSubscribe = result.subscribe.bind(result);
result.subscribe = (subscriber) => {
state.subscriberCount++;
if (state.subscriberCount === 1) {
startBackpressure();
}
const unsubscribe = originalSubscribe(subscriber);
return () => {
unsubscribe();
state.subscriberCount--;
if (state.subscriberCount === 0) {
cleanup();
}
};
};
// Override setValue on the source to handle backpressure
const originalSetValue = source.setValue;
source.setValue = (value) => {
const shouldApplyBackpressure = options.shouldApplyBackpressure?.() ?? (options.strategy !== types_1.BackpressureStrategy.Drop && state.valueCount >= bufferSize);
// Handle error strategy first
if (options.strategy === types_1.BackpressureStrategy.Error) {
if (state.isPaused || state.valueCount >= bufferSize || shouldApplyBackpressure) {
throw new Error("Backpressure limit exceeded");
}
state.valueCount++;
originalSetValue.call(source, value);
return;
}
// Handle other strategies
if (!state.isPaused && !shouldApplyBackpressure) {
state.valueCount++;
originalSetValue.call(source, value);
return;
}
switch (options.strategy) {
case types_1.BackpressureStrategy.Drop:
// Simply drop the value when backpressure is applied
break;
case types_1.BackpressureStrategy.Buffer:
if (state.valueCount < bufferSize || options.shouldApplyBackpressure) {
state.buffer.push(value);
state.valueCount++;
}
break;
case types_1.BackpressureStrategy.Sliding:
if (state.valueCount >= bufferSize) {
state.buffer.shift(); // Remove oldest value
state.valueCount--;
}
state.buffer.push(value);
state.valueCount++;
break;
default:
throw new Error(`Unknown backpressure strategy: ${options.strategy}`);
}
};
return result;
}
/**
* Creates a reflex that buffers values for a specified duration.
* Values received during the duration window are collected into an array
* and emitted together when the window closes.
*
* Example usage:
* ```typescript
* const source = reflex({ initialValue: 0 });
* const buffered = buffer(source, 1000); // Buffer for 1 second
*
* // If source emits: 1, 2, 3 within 1 second
* // buffered will emit: [1, 2, 3] after 1 second
* ```
*
* @param source The source reflex to buffer
* @param duration The duration in milliseconds to buffer values
* @returns A Reflex that emits arrays of buffered values
*/
function buffer(source, duration) {
const result = (0, reflex_1.reflex)({
initialValue: [],
});
const state = {
buffer: [],
isPaused: false,
valueCount: 0,
sourceUnsubscribe: null,
subscriberCount: 0,
timeoutId: null,
isInitialized: false,
lastEmitTime: 0,
};
const flushBuffer = () => {
if (state.buffer.length > 0) {
result.setValue([...state.buffer]);
state.buffer = [];
}
state.timeoutId = null;
};
const cleanup = () => {
if (state.timeoutId) {
clearTimeout(state.timeoutId);
state.timeoutId = null;
}
if (state.sourceUnsubscribe) {
state.sourceUnsubscribe();
state.sourceUnsubscribe = null;
}
state.buffer = [];
state.isInitialized = false;
};
const startBuffering = () => {
if (!state.sourceUnsubscribe) {
state.sourceUnsubscribe = source.subscribe((value) => {
if (!state.isInitialized) {
state.isInitialized = true;
return;
}
state.buffer.push(value);
if (!state.timeoutId) {
state.timeoutId = setTimeout(flushBuffer, duration);
}
});
}
};
// Override subscribe to manage buffering
const originalSubscribe = result.subscribe.bind(result);
result.subscribe = (subscriber) => {
state.subscriberCount++;
if (state.subscriberCount === 1) {
startBuffering();
}
const unsubscribe = originalSubscribe(subscriber);
return () => {
unsubscribe();
state.subscriberCount--;
if (state.subscriberCount === 0) {
cleanup();
}
};
};
return result;
}
/**
* Creates a reflex that samples the source reflex at the specified interval.
* The resulting reflex will emit the most recent value from the source
* at each interval, regardless of how many values the source has emitted.
*
* Example usage:
* ```typescript
* const source = reflex({ initialValue: 0 });
* const sampled = sample(source, 1000); // Sample every second
*
* // If source emits rapidly: 1, 2, 3, 4, 5
* // sampled might emit: 1, 3, 5 (at 1-second intervals)
* ```
*
* @param source The source reflex to sample
* @param interval The interval in milliseconds between samples
* @returns A Reflex that emits sampled values at the specified interval
*/
function sample(source, interval) {
const result = (0, reflex_1.reflex)({
initialValue: source.value,
});
const state = {
buffer: [],
isPaused: false,
valueCount: 0,
sourceUnsubscribe: null,
subscriberCount: 0,
timeoutId: null,
lastEmitTime: 0,
};
const startSampling = () => {
if (!state.timeoutId) {
state.timeoutId = setInterval(() => {
result.setValue(source.value);
}, interval);
}
};
const cleanup = () => {
if (state.timeoutId) {
clearInterval(state.timeoutId);
state.timeoutId = null;
}
};
// Override subscribe to manage sampling
const originalSubscribe = result.subscribe.bind(result);
result.subscribe = (subscriber) => {
state.subscriberCount++;
if (state.subscriberCount === 1) {
startSampling();
}
const unsubscribe = originalSubscribe(subscriber);
return () => {
unsubscribe();
state.subscriberCount--;
if (state.subscriberCount === 0) {
cleanup();
}
};
};
return result;
}
/**
* Creates a reflex that throttles the source reflex to emit at most once per specified duration.
* The throttled reflex will emit:
* 1. The initial value immediately
* 2. The first value in a new throttle window if it arrives early in the window
* 3. The last value received during the throttle window when the window ends
*
* Example usage:
* ```typescript
* const source = reflex({ initialValue: 0 });
* const throttled = throttle(source, 1000); // Throttle to at most one value per second
*
* // If source emits rapidly: 0, 1, 2, 3, 4, 5
* // throttled might emit: 0 (initial), 1 (first in window), 3 (last in window)
* ```
*
* @param source The source reflex to throttle
* @param duration The minimum time between emissions in milliseconds
* @returns A Reflex that emits throttled values
*/
function throttle(source, duration) {
const result = (0, reflex_1.reflex)({
initialValue: source.value,
});
const state = {
buffer: [],
isPaused: false,
valueCount: 0,
sourceUnsubscribe: null,
subscriberCount: 0,
timeoutId: null,
isInitialized: false,
pendingValue: null,
lastEmitTime: 0,
};
const emitValue = (value) => {
result.setValue(value);
state.lastEmitTime = Date.now();
state.pendingValue = null;
if (state.timeoutId) {
clearTimeout(state.timeoutId);
state.timeoutId = null;
}
};
const scheduleNextEmission = (value) => {
state.pendingValue = value;
if (!state.timeoutId) {
const remainingTime = Math.max(0, duration - (Date.now() - state.lastEmitTime));
state.timeoutId = setTimeout(() => {
if (state.pendingValue !== null) {
emitValue(state.pendingValue);
}
}, remainingTime);
}
};
const cleanup = () => {
if (state.timeoutId) {
clearTimeout(state.timeoutId);
state.timeoutId = null;
}
if (state.sourceUnsubscribe) {
state.sourceUnsubscribe();
state.sourceUnsubscribe = null;
}
state.pendingValue = null;
state.isInitialized = false;
state.lastEmitTime = 0;
};
const startThrottling = () => {
if (!state.sourceUnsubscribe) {
state.sourceUnsubscribe = source.subscribe((value) => {
const now = Date.now();
const timeSinceLastEmit = now - state.lastEmitTime;
// Always emit the initial value
if (!state.isInitialized) {
state.isInitialized = true;
emitValue(value);
return;
}
// If outside throttle window, emit immediately
if (timeSinceLastEmit >= duration) {
emitValue(value);
return;
}
// If this is the first value in a new throttle window and we're in the early phase,
// emit immediately
if (!state.timeoutId && !state.pendingValue && timeSinceLastEmit < duration / 3) {
emitValue(value);
return;
}
// Otherwise, schedule for later emission
scheduleNextEmission(value);
});
}
};
// Override subscribe to manage throttling
const originalSubscribe = result.subscribe.bind(result);
result.subscribe = (subscriber) => {
state.subscriberCount++;
if (state.subscriberCount === 1) {
startThrottling();
}
const unsubscribe = originalSubscribe(subscriber);
return () => {
unsubscribe();
state.subscriberCount--;
if (state.subscriberCount === 0) {
cleanup();
}
};
};
return result;
}
//# sourceMappingURL=backpressure.js.map