UNPKG

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
"use strict"; 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; }