@traxjs/trax
Version:
Reactive state management
155 lines (135 loc) • 5.53 kB
text/typescript
import { ProcessingContext } from "./types";
type SyncFunction = (...args: any[]) => any;
type AsyncGenFunction = (...args: any[]) => Generator<Promise<any>, void, any>;
function noop() { }
/**
* Wrap sync and generator-based async functions to produce an event stream processing context
* @param fn the function to wrap
* @param startProcessingContext a function that should start and return the processing context
* @param error a function that will be called when errors must be raised
* @param onProcessStart [optional] a callback called a the beginning of each processing step. Will stop the processing if it return false
* @param onProcessEnd [optional] a callback called at the end of each processing step
* @returns
*/
export function wrapFunction<F extends SyncFunction | AsyncGenFunction>(
fn: F,
startProcessingContext: () => ProcessingContext,
error: (msg: any) => void,
onProcessStart: () => false | void = noop,
onProcessEnd: (done: boolean) => void = noop,
): ReturnType<F> extends Generator ? (...args: Parameters<F>) => Promise<any> : (...args: Parameters<F>) => ReturnType<F> {
/** processing context associated to the wrapped function */
let processingContext: ProcessingContext | null = null;
/** Generator returned by the wrapped function in case of async functions */
let generator: Generator<Promise<any>, void, any> | undefined;
let generatorPromise: Promise<any> | undefined = undefined;
let genPromiseResolve: ((v: any) => void) | undefined;
let genPromiseReject: ((v: any) => void) | undefined;
return (...args: any[]) => {
onProcessStart();
processingContext = startProcessingContext();
let itr: IteratorResult<Promise<any>, void> | undefined;
let done = true;
generator = undefined;
let v: any = undefined;
try {
// compute will indirectly call registerDependency() through proxy getters
v = fn(...args);
if (!!v && v.next && typeof v.next === "function") {
generator = v;
// get the first iterator value
itr = v.next();
done = !!itr!.done;
} else if (!!v && typeof v === "object" && typeof v.then === "function" && typeof v.catch === "function") {
// catch promise errors
v.catch((err: any) => {
error(err);
});
}
} catch (ex) {
error(ex);
}
onProcessEnd(done);
if (done) {
processingContext.end();
processingContext = null;
} else {
processingContext.pause();
processIteratorPromise(itr, generator!);
}
if (generator !== undefined) {
if (generatorPromise) {
if (genPromiseReject) {
genPromiseReject("Processing Cancelled");
}
}
generatorPromise = genPromiseResolve = genPromiseReject = undefined;
if (done) {
return Promise.resolve(v);
} else {
// we create a promise that will resolve when the generator is done
generatorPromise = new Promise((resolve, reject) => {
genPromiseResolve = resolve;
genPromiseReject = reject;
});
}
return generatorPromise;
}
return v as any;
}
function processIteratorPromise(itr: IteratorResult<Promise<any>, void> | undefined, g: Generator<Promise<any>>) {
if (itr && !itr.done) {
const yieldValue = itr.value;
const logError = (err: any) => {
error(err);
}
if (!!yieldValue && (yieldValue as any).then) {
// yield returned a promise
yieldValue.then((v) => {
computeNext(v, g);
}).catch(logError);
} else {
// yield did not return a promise - let's create one
Promise.resolve().then(() => {
computeNext(yieldValue, g);
}).catch(logError);
}
}
}
function computeNext(nextValue: any, g: Generator<Promise<any>, void, any>) {
if (g !== generator || !processingContext) return;
// note: g !==generator when compute() was called before the previous generator processing ended
if (onProcessStart() === false) {
resetGenPromise();
return; // caller is disposed
}
processingContext.resume();
let itr: IteratorResult<Promise<any>, void> | undefined;
let done = true;
try {
// compute will indirectly call registerDependency() through proxy getters
itr = g.next(nextValue);
done = !!itr.done;
} catch (ex) {
error(ex);
done = true;
if (genPromiseReject) {
genPromiseReject(ex);
}
resetGenPromise();
}
onProcessEnd(done);
if (done) {
processingContext.end();
if (genPromiseResolve) {
genPromiseResolve(itr!.value);
}
} else {
processingContext.pause();
processIteratorPromise(itr, g);
}
}
function resetGenPromise() {
genPromiseResolve = genPromiseReject = undefined;
}
}