web-streams-extensions
Version:
A comprehensive collection of helper methods for WebStreams with built-in backpressure support, inspired by ReactiveExtensions
203 lines (202 loc) • 7.21 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.retryPipe = retryPipe;
exports.retryPipeValidated = retryPipeValidated;
const is_transform_js_1 = require("./utils/is-transform.cjs");
const through_js_1 = require("./operators/through.cjs");
function retryPipe(streamFactory, ...args) {
// Extract options from the end of arguments if present
let options = {};
let operators = args;
// Check if last argument is options
const lastArg = args[args.length - 1];
if (lastArg && typeof lastArg === 'object' && !lastArg.readable && !lastArg.writable && typeof lastArg.pipeThrough !== 'function') {
options = lastArg;
operators = args.slice(0, -1);
}
const { retries = 3, delay, highWaterMark = 1 } = options;
let reader = null;
let attempts = 0;
let cancelled = false;
let currentTimer = null;
async function cleanupReader() {
if (reader) {
try {
await reader.cancel();
}
catch (e) {
// Ignore cancel errors
}
try {
reader.releaseLock();
}
catch (e) {
// Ignore release errors
}
reader = null;
}
}
function clearTimer() {
if (currentTimer) {
clearTimeout(currentTimer);
currentTimer = null;
}
}
async function createStreamAndReader() {
await cleanupReader();
if (cancelled) {
return;
}
attempts++;
// Create a new stream and apply operators
const sourceStream = streamFactory();
let resultStream = sourceStream;
// Apply operators using the same pattern as pipe.ts
resultStream = operators
.map(x => (0, is_transform_js_1.isTransform)(x) ? (0, through_js_1.through)(x) : x)
.reduce((stream, operator) => {
return operator(stream, { highWaterMark });
}, resultStream);
reader = resultStream.getReader();
}
async function tryWithRetry(controller) {
let lastError;
const maxAttempts = retries + 1; // retries is additional attempts after initial
while (attempts < maxAttempts && !cancelled) {
try {
await createStreamAndReader();
// Read all values from the stream
while (!cancelled && reader) {
const { done, value } = await reader.read();
if (done) {
controller.close();
await cleanupReader();
return;
}
if (!cancelled) {
controller.enqueue(value);
}
}
return; // Success
}
catch (error) {
lastError = error;
await cleanupReader();
if (attempts >= maxAttempts || cancelled) {
break;
}
// Wait before retry if delay is specified
if (delay && !cancelled) {
await new Promise((resolve, reject) => {
currentTimer = setTimeout(() => {
currentTimer = null;
resolve();
}, delay);
});
}
}
}
// All retries failed or cancelled
if (!cancelled) {
controller.error(lastError || new Error("Cancelled"));
}
}
return new ReadableStream({
async start(controller) {
try {
await tryWithRetry(controller);
}
catch (error) {
if (!cancelled) {
controller.error(error);
}
}
},
async pull(controller) {
// Pull is handled in start() for retry logic
},
async cancel(reason) {
cancelled = true;
clearTimer();
await cleanupReader();
}
}, { highWaterMark });
}
/**
* Creates a retry pipe that validates the stream factory and operators work correctly.
* This version attempts to create and apply operators without consuming the stream,
* then returns a proper retryPipe stream for actual use.
*
* @template T The input stream type
* @param streamFactory Function that creates a new source stream for each attempt
* @param operators Stream operators to apply to each attempt
* @returns A promise that resolves to a retry-enabled stream
*/
async function retryPipeValidated(streamFactory, ...args) {
// Extract options from the end of arguments if present
let options = {};
let operators = args;
const lastArg = args[args.length - 1];
if (lastArg && typeof lastArg === 'object' && !lastArg.readable && !lastArg.writable) {
options = lastArg;
operators = args.slice(0, -1);
}
const { retries = 3, delay, highWaterMark = 1 } = options;
let attempts = 0;
let lastError;
let currentTimer = null;
const maxAttempts = retries + 1; // retries is additional attempts after initial
while (attempts < maxAttempts) {
try {
attempts++;
// Validate that we can create the stream and apply operators without consuming
const testStream = streamFactory();
let validationStream = testStream;
// Apply operators using the same pattern as pipe.ts
validationStream = operators
.map(x => (0, is_transform_js_1.isTransform)(x) ? (0, through_js_1.through)(x) : x)
.reduce((stream, operator) => {
return operator(stream, { highWaterMark });
}, validationStream);
// Just verify we can get a reader without consuming
const reader = validationStream.getReader();
try {
await reader.cancel(); // Properly cancel the test stream
}
catch (e) {
// Ignore cancel errors
}
try {
reader.releaseLock();
}
catch (e) {
// Ignore release errors
}
// Validation succeeded, return a proper retryPipe
return retryPipe(streamFactory, ...args);
}
catch (error) {
lastError = error;
if (attempts >= maxAttempts) {
throw lastError;
}
// Wait before retry if delay is specified
if (delay && attempts < maxAttempts) {
await new Promise((resolve) => {
currentTimer = setTimeout(() => {
currentTimer = null;
resolve();
}, delay);
});
}
}
finally {
// Clean up any timer
if (currentTimer) {
clearTimeout(currentTimer);
currentTimer = null;
}
}
}
throw lastError;
}