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
JavaScript
;
/**
* 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);
}