UNPKG

web-streams-extensions

Version:

A comprehensive collection of helper methods for WebStreams with built-in backpressure support, inspired by ReactiveExtensions

139 lines (138 loc) 4.57 kB
"use strict"; /** * Combines multiple ReadableStreams by emitting an array/tuple of the latest values from each source * when all sources have emitted. Completes when any source completes. * * @example * ```typescript * // Basic zip - returns tuples * const numbers = from([1, 2, 3]); * const letters = from(['a', 'b', 'c']); * const zipped = zip(numbers, letters); * // Emits: [1, 'a'], [2, 'b'], [3, 'c'] * * // With selector function * const combined = zip(numbers, letters, (n, l) => `${n}${l}`); * // Emits: "1a", "2b", "3c" * * // Three streams * const symbols = from(['!', '?', '.']); * const tripleZip = zip(numbers, letters, symbols); * // Emits: [1, 'a', '!'], [2, 'b', '?'], [3, 'c', '.'] * ``` */ Object.defineProperty(exports, "__esModule", { value: true }); exports.zip = zip; // Implementation function zip(...args) { // Handle legacy array format if (args.length === 1 && Array.isArray(args[0])) { return zipArray(args[0]); } // Extract selector if it's the last argument and a function const lastArg = args[args.length - 1]; const hasSelector = typeof lastArg === 'function'; const selector = hasSelector ? lastArg : null; const sources = hasSelector ? args.slice(0, -1) : args; // Create the base zip stream const baseStream = zipSources(sources); // Apply selector if provided if (selector) { return baseStream.pipeThrough(new TransformStream({ transform(chunk, controller) { try { const result = selector(...chunk); controller.enqueue(result); } catch (error) { controller.error(error); } } })); } return baseStream; } /** * Core zip implementation for multiple sources */ function zipSources(sources) { let readers = null; return new ReadableStream({ async start() { // Handle empty array case immediately if (sources.length === 0) { readers = []; return; } readers = sources.map(s => s.getReader()); }, async pull(controller) { try { if (!readers) return; // Handle empty sources case if (readers.length === 0) { controller.close(); return; } const results = await Promise.all(readers.map(r => r.read())); const isDone = results.some(result => result.done); if (isDone) { controller.close(); // Release readers that aren't done results.forEach((result, index) => { if (!result.done && readers[index]) { try { readers[index].releaseLock(); } catch (e) { // Ignore cleanup errors } } }); readers = null; } else { const values = results.map(result => result.value); controller.enqueue(values); } } catch (error) { controller.error(error); // Cleanup on error if (readers) { await Promise.all(readers.map(async (reader) => { try { await reader.cancel(error); reader.releaseLock(); } catch (e) { // Ignore cleanup errors } })); readers = null; } } }, async cancel(reason) { if (readers) { await Promise.all(readers.map(async (reader) => { try { await reader.cancel(reason); reader.releaseLock(); } catch (e) { // Ignore cleanup errors } })); readers = null; } } }); } /** * Legacy implementation for array of homogeneous streams */ function zipArray(sources) { return zipSources(sources); }