monoflux
Version:
Stream processing for JS simplified
581 lines • 19.5 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.Flux = void 0;
class Flux {
generator;
upstream;
handleCancel;
_closed = false;
constructor(generator, upstream, handleCancel) {
const thiss = this;
this.generator = (async function* gen() {
try {
yield* generator;
}
catch (e) {
yield ignorableValue;
throw e;
}
finally {
thiss._closed = true;
}
})();
this.upstream = upstream;
this.handleCancel = handleCancel;
}
//// Start AsyncGenerator methods ////
[Symbol.asyncIterator]() {
return this;
}
async next(...args) {
const value = await this.generator.next(args);
return value.value !== ignorableValue
? value
: await this.next(args);
}
return(value) {
return this.generator.return(value);
}
throw(e) {
return this.generator.throw(e);
}
async cancel(e) {
if (this.closed) {
return;
}
if (this.upstream !== undefined) {
return this.upstream.cancel(e);
}
else {
if (this.handleCancel !== undefined) {
this.handleCancel(e);
}
else {
await this.throw(e);
}
}
}
//// End AsyncGenerator methods ////
//// Start Promise methods ////
async then(onfulfilled, onrejected) {
try {
const result = await this.asList();
return onfulfilled !== undefined && onfulfilled !== null
? await onfulfilled(result)
: result;
}
catch (e) {
if (onrejected !== undefined && onrejected !== null) {
return onrejected(e);
}
else {
throw e;
}
}
}
async catch(onrejected) {
try {
return this.asList();
}
catch (e) {
if (onrejected !== undefined && onrejected !== null) {
return onrejected(e);
}
else {
throw e;
}
}
}
async finally(onfinally) {
const promise = await this.asList();
onfinally?.();
return promise;
}
get [Symbol.toStringTag]() {
return 'Flux';
}
//// End Promise methods ////
subscribe(callback) {
const flux = callback !== undefined
? this.doOnEach(callback)
: this;
const recur = () => {
flux
.next()
.then(value => {
if (!value.done) {
recur();
}
})
.catch(err => {
console.error(err);
});
};
recur();
return {
unsubscribe: () => this.return()
};
}
filter(predicate) {
const thiss = this;
return Flux.constructFromGeneratorFunction(async function* gen() {
for await (const value of thiss) {
if (predicate(value)) {
yield value;
}
}
}, this);
}
/**
* Flux until predicate is true; the rest is dropped. The first
* value that is dropped is the value for which the predicate
* is true.
*
* @param predicate
*/
untilExcl(predicate) {
const thiss = this;
return Flux.constructFromGeneratorFunction(async function* gen() {
for await (const value of thiss) {
if (predicate(value)) {
break;
}
yield value;
}
}, this);
}
doOnEach(callback) {
const thiss = this;
return Flux.constructFromGeneratorFunction(async function* gen() {
for await (const value of thiss) {
callback(value);
yield value;
}
}, this);
}
doAfterLast(callback) {
const thiss = this;
const events = [];
return Flux.constructFromGeneratorFunction(async function* gen() {
for await (const value of thiss) {
events.push(value);
yield value;
}
await callback(events);
}, this);
}
map(mapper) {
const thiss = this;
return Flux.constructFromGeneratorFunction(async function* gen() {
for await (const value of thiss) {
yield mapper(value);
}
}, this);
}
take(n) {
const thiss = this;
return Flux.constructFromGeneratorFunction(async function* gen() {
let i = 0;
for await (const value of thiss) {
if (i >= n) {
thiss.cancel();
break;
}
i++;
yield value;
}
}, this);
}
/**
* Delay each of this Flux elements (Subscriber.onNext signals) by a given duration.
* Signals are delayed and continue on the parallel default Scheduler, but empty
* sequences or immediate error signals are not delayed.
*
* @param delayMillis - The duration in milliseconds to delay each element emission
* @returns A new Flux with delayed element emissions
*
* @example
* ```typescript
* const flux = Flux.fromArray([1, 2, 3])
* const delayed = flux.delayElements(1000) // Each element delayed by 1 second
* for await (const value of delayed) {
* console.log(value) // Prints 1, 2, 3 with 1 second delay between each
* }
* ```
*/
delayElements(delayMillis) {
const thiss = this;
return Flux.constructFromGeneratorFunction(async function* gen() {
for await (const value of thiss) {
await new Promise(resolve => setTimeout(resolve, delayMillis));
yield value;
}
}, this);
}
flatMap(mapper, options) {
const thiss = this;
return Flux.constructFromGeneratorFunction(async function* gen() {
// Get first value to determine type
const iterator = thiss[Symbol.asyncIterator]();
const firstValue = await iterator.next();
if (firstValue.done) {
return;
}
const firstResult = mapper(firstValue.value);
const concurrency = options?.concurrency;
if (Array.isArray(firstResult)) {
yield* flatMapArrayImpl(mapper, firstValue.value, firstResult, iterator);
}
else if (firstResult instanceof Flux) {
yield* flatMapFluxImpl(mapper, firstValue.value, firstResult, iterator, concurrency);
}
else {
yield* flatMapPromiseImpl(mapper, firstValue.value, firstResult, iterator, concurrency);
}
}, this);
}
transform(defineGenerator) {
const definedGenerator = defineGenerator(this);
return Flux.constructFromGeneratorFunction(async function* gen() {
for await (const value of definedGenerator) {
yield value;
}
}, this);
}
async reduce(reducer, initialValue) {
let reduction = initialValue;
for await (const value of this) {
reduction = reducer(reduction, value);
}
return reduction;
}
async asList() {
const result = [];
for await (const value of this) {
result.push(value);
}
return result;
}
async whenComplete() {
for await (const ignored of this) {
// do nothing
}
}
static create(creator) {
const values = [];
const resolvers = [];
let isDone = false;
let error = undefined;
const push = (value) => {
if (isDone) {
throw new Error("Cannot push to a completed generator");
}
if (resolvers.length > 0) {
const resolve = resolvers.shift();
resolve.resolve({ done: false, value });
}
else {
values.push(value);
}
};
const complete = () => {
isDone = true;
while (resolvers.length > 0) {
const resolve = resolvers.shift();
resolve.resolve({ done: true, value: undefined });
}
};
const reject = (err) => {
error = { value: err };
// Wake up next resolver to propagate the error
resolvers.shift()?.reject(err); // This will make the `.next()` promise reject with the error
};
(async () => {
try {
await creator(push, complete, reject);
if (!isDone || error === undefined) {
complete();
}
}
catch (err) {
reject(err);
}
})();
return Flux.fromGenerator({
[Symbol.asyncIterator]() {
return this;
},
next(...args) {
if (error) {
const err = error.value;
error = undefined;
return Promise.reject(err);
}
if (values.length > 0) {
const value = values.shift();
return Promise.resolve({ done: false, value });
}
if (isDone) {
return Promise.resolve({ done: true, value: undefined });
}
return new Promise((resolve, reject) => {
resolvers.push({ resolve, reject });
});
},
return() {
complete();
return Promise.resolve({ done: true, value: undefined });
},
throw(err) {
reject(err);
return Promise.reject(err);
},
});
}
static just(...array) {
return Flux.from(array);
}
static from(source, handleCancel) {
if (Array.isArray(source)) {
return Flux.fromArray(source);
}
else if (typeof source === 'function') {
return Flux.fromGeneratorFunction(source, handleCancel);
}
else if (source instanceof ReadableStream) {
return Flux.fromReadableStream(source, handleCancel);
}
else if (isGenerator(source)) {
return Flux.fromGenerator(source, handleCancel);
}
else {
throw new TypeError('Source must be an Array, AsyncGenerator, ReadableStream, or a generator function');
}
}
static fromArray(array) {
return Flux.fromGeneratorFunction(async function* gen() {
for (const value of array) {
yield value;
}
});
}
static constructFromGeneratorFunction(fn, upstream, handleCancel) {
return new Flux(fn(), upstream, handleCancel);
}
static fromGeneratorFunction(fn, handleCancel) {
return Flux.constructFromGeneratorFunction(fn, undefined, handleCancel);
}
static fromGenerator(generator, handleCancel) {
return new Flux(generator, undefined, handleCancel);
}
static fromReadableStream(stream, handleCancel) {
return Flux.fromGeneratorFunction(async function* gen() {
const reader = stream.getReader();
try {
let excerpt = undefined;
while (!(excerpt = await reader.read()).done) {
yield excerpt.value;
}
}
finally {
await reader.cancel();
}
}, handleCancel);
}
get closed() {
return this._closed;
}
}
exports.Flux = Flux;
function flatMapArrayImpl(mapper, firstValue, firstResult, iterator) {
return (async function* () {
// Yield first result
for (const item of firstResult) {
yield item;
}
// Continue with remaining values
let next = await iterator.next();
while (!next.done) {
const result = mapper(next.value);
for (const item of result) {
yield item;
}
next = await iterator.next();
}
})();
}
function flatMapPromiseImpl(mapper, firstValue, firstPromise, iterator, concurrency) {
return (async function* () {
const pendingPromises = [];
const resolvedValues = [];
const resolvedErrors = [];
const resolvedIndexes = new Set();
let yieldedUpTo = 0;
let nextIndex = 0;
let sourceComplete = false;
let runningCount = 0;
// Helper to start a promise for a value
const startPromise = (value, index, promise) => {
const p = promise || mapper(value);
pendingPromises[index] = p;
p.then(resolved => {
resolvedValues[index] = resolved;
resolvedIndexes.add(index);
if (concurrency !== undefined) {
runningCount--;
}
}).catch(error => {
resolvedErrors[index] = error;
resolvedIndexes.add(index);
if (concurrency !== undefined) {
runningCount--;
}
});
if (concurrency !== undefined) {
runningCount++;
}
};
// Start first promise
startPromise(firstValue, nextIndex++, firstPromise);
// Process iterator in background
const processIterator = async () => {
let next = await iterator.next();
while (!next.done) {
// Wait if concurrency limit reached
if (concurrency !== undefined) {
while (runningCount >= concurrency) {
await new Promise(resolve => setTimeout(resolve, 0));
}
}
startPromise(next.value, nextIndex++);
next = await iterator.next();
}
sourceComplete = true;
};
// Start processing iterator in background
const iteratorPromise = processIterator();
// Yield results in order as they become available
while (true) {
// Wait for next result to be available
while (!resolvedIndexes.has(yieldedUpTo)) {
// Check if we're done
if (sourceComplete && yieldedUpTo >= pendingPromises.length) {
return;
}
await new Promise(resolve => setTimeout(resolve, 0));
}
// Check if we're done
if (sourceComplete && yieldedUpTo >= pendingPromises.length) {
break;
}
// Yield the next result
if (resolvedErrors[yieldedUpTo] !== undefined) {
throw resolvedErrors[yieldedUpTo];
}
if (resolvedValues[yieldedUpTo] !== undefined) {
yield resolvedValues[yieldedUpTo];
}
yieldedUpTo++;
}
// Wait for iterator to complete
await iteratorPromise;
})();
}
function flatMapFluxImpl(mapper, firstValue, firstFlux, iterator, concurrency) {
return (async function* () {
// Collect all values first
const allValues = [firstValue];
let next = await iterator.next();
while (!next.done) {
allValues.push(next.value);
next = await iterator.next();
}
const pendingFluxes = new Array(allValues.length);
const buffers = new Map();
const completedIndexes = new Set();
let yieldedUpTo = 0;
if (concurrency === undefined) {
// No concurrency limit - start all fluxes immediately
for (let i = 0; i < allValues.length; i++) {
const index = i; // Capture the index value
const flux = index === 0 ? firstFlux : mapper(allValues[index]);
pendingFluxes[index] = flux;
buffers.set(index, []);
(async () => {
try {
for await (const item of flux) {
buffers.get(index).push(item);
}
}
catch (error) {
buffers.get(index).push({ __error: error });
}
finally {
completedIndexes.add(index);
}
})();
}
}
else {
// Concurrency limited - use semaphore pattern
let currentIndex = 0;
let runningCount = 0;
const startNext = () => {
while (runningCount < concurrency && currentIndex < allValues.length) {
const index = currentIndex;
currentIndex++;
runningCount++;
const flux = index === 0 ? firstFlux : mapper(allValues[index]);
pendingFluxes[index] = flux;
buffers.set(index, []);
(async () => {
try {
for await (const item of flux) {
buffers.get(index).push(item);
}
}
catch (error) {
buffers.get(index).push({ __error: error });
}
finally {
completedIndexes.add(index);
runningCount--;
startNext();
}
})();
}
};
startNext();
}
// Yield results in order
while (yieldedUpTo < pendingFluxes.length) {
const buffer = buffers.get(yieldedUpTo);
// Keep processing this flux until it's completed AND buffer is empty
while (!completedIndexes.has(yieldedUpTo) || buffer.length > 0) {
// Wait for buffer to have items or for flux to complete
while (buffer.length === 0 && !completedIndexes.has(yieldedUpTo)) {
await new Promise(resolve => setTimeout(resolve, 1));
}
// Yield all items currently in buffer
while (buffer.length > 0) {
const item = buffer.shift();
if (item && typeof item === 'object' && '__error' in item) {
throw item.__error;
}
yield item;
}
}
yieldedUpTo++;
}
})();
}
function isGenerator(obj) {
return obj !== null &&
typeof obj === 'object' &&
typeof obj.next === 'function' &&
typeof obj.return === 'function' &&
typeof obj.throw === 'function' &&
Symbol.asyncIterator in obj;
}
const ignorableValue = {};
//# sourceMappingURL=Flux.js.map