Skip to main content
Module

x/evt/lib/Evt.ts

💧EventEmitter's typesafe replacement
Go to Latest
File
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971
import "https://raw.githubusercontent.com/garronej/minimal_polyfills/v2.2.1/deno_dist/Array.prototype.find.ts";import { importProxy } from "./importProxy.ts";import { create } from "./Evt.create.ts";import { getCtxFactory } from "./Evt.getCtx.ts";import { factorize } from "./Evt.factorize.ts";import { merge } from "./Evt.merge.ts";import { from } from "./Evt.from.ts";import { useEffect } from "./Evt.useEffect.ts";import { asPostable } from "./Evt.asPostable.ts";import { asyncPipe } from "./Evt.asyncPipe.ts";import { asNonPostable } from "./Evt.asNonPostable.ts";import { parsePropsFromArgs, matchAll } from "./Evt.parsePropsFromArgs.ts";import { newCtx } from "./Evt.newCtx.ts";import { LazyEvt } from "./LazyEvt.ts";import { defineAccessors } from "../tools/typeSafety/defineAccessors.ts";import { invokeOperator } from "./util/invokeOperator.ts";import { Polyfill as Map, LightMap } from "https://raw.githubusercontent.com/garronej/minimal_polyfills/v2.2.1/deno_dist/Map.ts";import { Polyfill as WeakMap } from "https://raw.githubusercontent.com/garronej/minimal_polyfills/v2.2.1/deno_dist/WeakMap.ts";import * as runExclusive from "https://raw.githubusercontent.com/garronej/run_exclusive/v2.2.14/deno_dist/mod.ts";import { overwriteReadonlyProp } from "../tools/typeSafety/overwriteReadonlyProp.ts";import { typeGuard } from "../tools/typeSafety/typeGuard.ts";import { encapsulateOpState } from "./util/encapsulateOpState.ts";import { Deferred } from "../tools/Deferred.ts";import { loosenType } from "./Evt.loosenType.ts";import { safeClearTimeout, safeSetTimeout, Timer } from "../tools/safeSetTimeout.ts";import { isPromiseLike } from "../tools/typeSafety/isPromiseLike.ts";
import type { Handler } from "./types/Handler.ts";import * as _1 from "./types/Operator.ts";import * as _2 from "./types/EvtError.ts";import * as _3 from "./types/interfaces/CtxLike.ts";
type NonPostableEvt<T> = import("./types/interfaces/index.ts").NonPostableEvt<T>;type StatefulEvt<T> = import("./types/interfaces/index.ts").StatefulEvt<T>;

/** https://docs.evt.land/api/evt */export type Evt<T> = import("./types/interfaces/index.ts").Evt<T>;
class EvtImpl<T> implements Evt<T> {
static readonly create = create;
static readonly newCtx = newCtx;
static readonly merge = merge;
static readonly from = from;
static readonly useEffect = useEffect;
static readonly getCtx = getCtxFactory();
static readonly loosenType = loosenType;
static readonly factorize = factorize;
static readonly asPostable = asPostable;
static readonly asyncPipe = asyncPipe;
static readonly asNonPostable = asNonPostable;
private static __defaultMaxHandlers = 25;
static setDefaultMaxHandlers(n: number): void { this.__defaultMaxHandlers = isFinite(n) ? n : 0; }
toStateful(p1: any, p2?: _3.CtxLike): StatefulEvt<any> {
const isP1Ctx = _3.z_3.match(p1);
const initialValue: any = isP1Ctx ? undefined : p1; const ctx = p2 || (isP1Ctx ? p1 : undefined);
const out = new importProxy.StatefulEvt<any>(initialValue);
const callback = (data: T) => out.post(data);
if (!!ctx) { this.attach(ctx, callback); } else { this.attach(callback); }
return out;
}
declare readonly evtAttach: Evt<Handler<T, any>>; declare readonly evtDetach: Evt<Handler<T, any>>;
private readonly lazyEvtAttach = new LazyEvt<Handler<T, any>>(); private readonly lazyEvtDetach = new LazyEvt<Handler<T, any>>();
private static __1: void = (() => {
if (false) { EvtImpl.__1 }
defineAccessors( EvtImpl.prototype, "evtAttach", { "get": function (this: EvtImpl<any>) { return this.lazyEvtAttach.evt; } } );
defineAccessors( EvtImpl.prototype, "evtDetach", { "get": function (this: EvtImpl<any>) { return this.lazyEvtDetach.evt; } } );
})();
private __maxHandlers: undefined | number = undefined;
setMaxHandlers(n: number): this { this.__maxHandlers = isFinite(n) ? n : 0; return this; }
readonly postCount: number = 0;
private traceId: string | null = null; private traceFormatter!: (data: T) => string; private log!: Exclude<Parameters<NonPostableEvt<any>["enableTrace"]>[0]["log"], false>;
enableTrace( params: { id: string, formatter?: (data: T) => string, log?: ((message?: any, ...optionalParams: any[]) => void) | false } //NOTE: Not typeof console.log as we don't want to expose types from node ): void {
const { id, formatter, log } = params;
this.traceId = id;
this.traceFormatter = formatter || ( data => { try { return JSON.stringify(data, null, 2); } catch { return `${data}`; } } );
this.log = log === undefined ? ((...inputs) => console.log(...inputs)) : log === false ? undefined : log ;
}
disableTrace(): this { this.traceId = null; return this; }
private readonly handlers: Handler<T, any>[] = [];
private readonly handlerTriggers: LightMap< Handler<T, any>, (opResult: _1.Operator..Result.Matched<any, any>) => PromiseLike<void> | undefined > = new Map();
//NOTE: An async handler ( attached with waitFor ) is only eligible to handle a post if the post //occurred after the handler was set. We don't want to waitFor event from the past. //private readonly asyncHandlerChronologyMark = new WeakMap<ImplicitParams.Async, number>(); declare private readonly asyncHandlerChronologyMark: WeakMap< Handler.PropsFromMethodName.Async, number >; declare private __asyncHandlerChronologyMark: (typeof EvtImpl.prototype.asyncHandlerChronologyMark) | undefined;
//NOTE: There is an exception to the above rule, we want to allow async waitFor loop //do so we have to handle the case where multiple event would be posted synchronously. declare private readonly asyncHandlerChronologyExceptionRange: WeakMap< Handler.PropsFromMethodName.Async, { lowerMark: number; upperMark: number; } >; declare private __asyncHandlerChronologyExceptionRange: (typeof EvtImpl.prototype.asyncHandlerChronologyExceptionRange) | undefined;
declare private readonly statelessByStatefulOp: WeakMap< _1.Operator..Stateful<T, any, any>, _1.Operator..Stateless<T, any, any> >; declare private __statelessByStatefulOp: (typeof EvtImpl.prototype.statelessByStatefulOp) | undefined;
private static __2: void = (() => {
if (false) { EvtImpl.__2; }
Object.defineProperties(EvtImpl.prototype, ([ "__asyncHandlerChronologyMark", "__asyncHandlerChronologyExceptionRange", "__statelessByStatefulOp" ] as const).map(key => [ key.substr(2), { "get": function (this: EvtImpl<any>) {
if (this[key] === undefined) { this[key] = new WeakMap<any, any>(); }
return this[key];
} } ] as const).reduce<any>((prev, [key, obj]) => ({ ...prev, [key]: obj }), {}) );

})();
/* NOTE: Used as Date.now() would be used to compare if an event is anterior or posterior to an other. We don't use Date.now() because two call within less than a ms will return the same value unlike this function. */ private __currentChronologyMark = 0; private getChronologyMark() { return this.__currentChronologyMark++; }

private asyncHandlerCount: number = 0;
private detachHandler( handler: Handler<T, any>, wTimer: [Timer | undefined], rejectPr: (error: _2.EvtError.Detached) => void ) {
const index = this.handlers.indexOf(handler);
if (index < 0) { return false; }
if (typeGuard<Handler<T, any, _3.CtxLike<any>>>(handler, !!handler.ctx)) { handler.ctx.zz__removeHandler(handler); }

this.handlers.splice(index, 1);
if (handler.async) { this.asyncHandlerCount--; }
this.handlerTriggers.delete(handler);
if (wTimer[0] !== undefined) {
safeClearTimeout(wTimer[0]);
rejectPr(new _2.EvtError.Detached());
}
this.lazyEvtDetach.post(handler);
return true;
}

private triggerHandler<U>( handler: Handler<T, U>, wTimer: [Timer | undefined], resolvePr: ((transformedData: any) => void) | undefined, opResult: _1.Operator..Result.Matched<any, any> ): PromiseLike<void> | undefined {
const { callback, once } = handler;
if (wTimer[0] !== undefined) { safeClearTimeout(wTimer[0]); wTimer[0] = undefined; }
doDetachIfNeeded(handler, opResult, once);
const [transformedData] = opResult;
const prOrValue = callback?.call( this, transformedData );
resolvePr?.(transformedData);
return isPromiseLike(prOrValue) ? prOrValue : undefined;
}
private addHandler<U>( propsFromArgs: Handler.PropsFromArgs<T, U>, propsFromMethodName: Handler.PropsFromMethodName ): Handler<T, U> {

if (_1.z_f1.fλ_Stateful_match<T, any, any>(propsFromArgs.op)) {
this.statelessByStatefulOp.set( propsFromArgs.op, encapsulateOpState(propsFromArgs.op) );
}
const d = new Deferred<U>();
const wTimer: [Timer | undefined] = [undefined];
const handler: Handler<T, U> = { ...propsFromArgs, ...propsFromMethodName, "detach": () => this.detachHandler(handler, wTimer, d.reject), "promise": d.pr };
if (typeof handler.timeout === "number") {
wTimer[0] = safeSetTimeout(() => {
wTimer[0] = undefined;
handler.detach();
d.reject(new _2.EvtError.Timeout(handler.timeout!));
}, handler.timeout);
}
this.handlerTriggers.set( handler, opResult => this.triggerHandler( handler, wTimer, d.isPending ? d.resolve : undefined, opResult ) );
if (handler.async) {
this.asyncHandlerChronologyMark.set( handler, this.getChronologyMark() );
}
if (handler.prepend) {
let i: number;
for (i = 0; i < this.handlers.length; i++) {
if (this.handlers[i].extract) { continue; }
break;
}
this.handlers.splice(i, 0, handler);
} else {
this.handlers.push(handler);
}
if (handler.async) { this.asyncHandlerCount++; }
this.checkForPotentialMemoryLeak();
if (typeGuard<Handler<T, U, _3.CtxLike<any>>>(handler, !!handler.ctx)) { handler.ctx.zz__addHandler(handler, this); }
this.lazyEvtAttach.post(handler);
return handler;
}


private checkForPotentialMemoryLeak(): void {
const maxHandlers = this.__maxHandlers !== undefined ? this.__maxHandlers : EvtImpl.__defaultMaxHandlers ;

if ( maxHandlers === 0 || this.handlers.length % (maxHandlers + 1) !== 0) { return; }
let message = [ `MaxHandlersExceededWarning: Possible Evt memory leak detected.`, `${this.handlers.length} handlers attached${this.traceId ? ` to "${this.traceId}"` : ""}.\n`, `Use Evt.prototype.setMaxHandlers(n) to increase limit on a specific Evt.\n`, `Use Evt.setDefaultMaxHandlers(n) to change the default limit currently set to ${EvtImpl.__defaultMaxHandlers}.\n`, ].join("");
const map = new Map<string, number>();
this.getHandlers() .map(({ ctx, async, once, prepend, extract, op, callback }) => ({ "hasCtx": !!ctx, once, prepend, extract, "isWaitFor": async, ...(op === matchAll ? {} : { "op": op.toString() }), ...(!callback ? {} : { "callback": callback.toString() }) })) .map(obj => "{\n" + Object.keys(obj) .map(key => ` ${key}: ${(obj as any)[key]}`) .join(",\n") + "\n}" ) .forEach(str => map.set(str, (map.has(str) ? map.get(str)! : 0) + 1)) ;
message += "\n" + Array.from(map.keys()) .map(str => `${map.get(str)} handler${map.get(str) === 1 ? "" : "s"} like:\n${str}`) .join("\n") + "\n";
if (this.traceId === null) {
message += "\n" + [ `To validate the identify of the Evt instance that is triggering this warning you can call`, `Evt.prototype.enableTrace({ "id": "My evt id", "log": false }) on the Evt that you suspect.\n` ].join(" ");
}
try { console.warn(message); } catch { }
}
getStatelessOp<U, CtxResult>(op: _1.Operator<T, U, CtxResult>): _1.Operator.Stateless<T, U, CtxResult> { return _1.z_f1.fλ_Stateful_match(op) ? this.statelessByStatefulOp.get(op)! : op }
private trace(data: T) {
if (this.traceId === null) { return; }
let message = `(${this.traceId}) `;
const isExtracted = !!this.handlers.find( ({ extract, op }) => ( extract && !!this.getStatelessOp(op)(data) ) );
if (isExtracted) {
message += "extracted ";
} else {
const handlerCount = this.handlers .filter( ({ extract, op }) => !extract && !!this.getStatelessOp(op)(data) ) .length;
message += `${handlerCount} handler${(handlerCount > 1) ? "s" : ""}, `;
}
this.log?.(message + this.traceFormatter(data));
}
/** Return [ isExtracted, prAllHandlerCallbacksResolved ] */ private postSync(data: T): readonly [boolean, Promise<void>] {
const prAllHandlerCallbacksResolved: PromiseLike<void>[] = [];
const getReturnValue = (isExtracted: boolean) => [ isExtracted, Promise.all(prAllHandlerCallbacksResolved).then(() => { }) ] as const;

for (const handler of [...this.handlers]) {
const { async, op, extract } = handler;
if (async) { continue; }
const opResult = invokeOperator( this.getStatelessOp(op), data, true );
if (_1.z_f1.fλ_Result_NotMatched_match(opResult)) {
doDetachIfNeeded(handler, opResult);
continue;
}
const handlerTrigger = this.handlerTriggers.get(handler);
//NOTE: Possible if detached while in the loop. if (!handlerTrigger) { continue; }
const prOrUndefined = handlerTrigger(opResult);
if (prOrUndefined !== undefined) { prAllHandlerCallbacksResolved.push(prOrUndefined); }
if (extract) { return getReturnValue(true); }
}
return getReturnValue(false);
}
private postAsyncFactory() { return runExclusive.buildMethodCb( (data: T, postChronologyMark: number, releaseLock?) => {
if (this.asyncHandlerCount === 0) { releaseLock(); return; }
const promises: Promise<void>[] = [];
let chronologyMarkStartResolveTick: number;
//NOTE: Must be before handlerTrigger call. Promise.resolve().then( () => chronologyMarkStartResolveTick = this.getChronologyMark() );

for (const handler of [...this.handlers]) {
if (!handler.async) { continue; }
const opResult = invokeOperator( this.getStatelessOp(handler.op), data, true );
if (_1.z_f1.fλ_Result_NotMatched_match(opResult)) {
doDetachIfNeeded(handler, opResult);
continue;
}
const handlerTrigger = this.handlerTriggers.get(handler);
if (!handlerTrigger) { continue; }
const shouldCallHandlerTrigger = (() => {
const handlerMark = this.asyncHandlerChronologyMark.get(handler)!;
if (postChronologyMark > handlerMark) { return true; }
const exceptionRange = this.asyncHandlerChronologyExceptionRange.get(handler);
return ( exceptionRange !== undefined && exceptionRange.lowerMark < postChronologyMark && postChronologyMark < exceptionRange.upperMark && handlerMark > exceptionRange.upperMark );
})();
if (!shouldCallHandlerTrigger) { continue; }
promises.push( new Promise<void>( resolve => handler.promise .then(() => resolve()) .catch(() => resolve()) ) );
handlerTrigger(opResult);

}
if (promises.length === 0) { releaseLock(); return; }
const handlersDump = [...this.handlers];
Promise.all(promises).then(() => {
for (const handler of this.handlers) {
if (!handler.async) { continue; }
if (handlersDump.indexOf(handler) >= 0) { continue; }
this.asyncHandlerChronologyExceptionRange.set( handler, { "lowerMark": postChronologyMark, "upperMark": chronologyMarkStartResolveTick } );
}
releaseLock();
});
} ); }
declare private postAsync: ( ( data: T, postChronologyMark: number ) => void ) | undefined;
private static readonly propsFormMethodNames: Record< "waitFor" | "attach" | "attachExtract" | "attachPrepend" | "attachOnce" | "attachOncePrepend" | "attachOnceExtract" , Handler.PropsFromMethodName > = { "waitFor": { "async": true, "extract": false, "once": true, "prepend": false }, "attach": { "async": false, "extract": false, "once": false, "prepend": false }, "attachExtract": { "async": false, "extract": true, "once": false, "prepend": true }, "attachPrepend": { "async": false, "extract": false, "once": false, "prepend": true }, "attachOnce": { "async": false, "extract": false, "once": true, "prepend": false }, "attachOncePrepend": { "async": false, "extract": false, "once": true, "prepend": true }, "attachOnceExtract": { "async": false, "extract": true, "once": true, "prepend": true } };
isHandled(data: T): boolean { return !!this.getHandlers() .find(({ op }) => !!this.getStatelessOp(op)(data)) ; }
getHandlers(): Handler<T, any>[] { return [...this.handlers]; }
detach(ctx?: _3.CtxLike<any>): Handler<T, any, any>[] {
const detachedHandlers: Handler<T, any>[] = [];
for (const handler of this.getHandlers()) {
if (ctx !== undefined && handler.ctx !== ctx) { continue; }
const wasStillAttached = handler.detach();
//NOTE: It should not be possible. if (!wasStillAttached) { continue; }
detachedHandlers.push(handler);
}
return detachedHandlers;
}
pipe(...args: any[]): Evt<any> {
const evtDelegate = new EvtImpl<any>();
this.addHandler( { ...parsePropsFromArgs<T>(args, "pipe"), "callback": (transformedData: any) => evtDelegate.post(transformedData) }, EvtImpl.propsFormMethodNames.attach );
return evtDelegate;
}
waitFor(...args: any[]): Promise<any> { return this.addHandler( parsePropsFromArgs<T>(args, "waitFor"), EvtImpl.propsFormMethodNames.waitFor ).promise; }
$attach(...inputs: any[]) { return (this.attach as any)(...inputs); }
attach(...args: any[]) { return this.__attachX(args, "attach"); }
$attachOnce(...inputs: any[]) { return (this.attachOnce as any)(...inputs); }
attachOnce(...args: any[]) { return this.__attachX(args, "attachOnce"); }
$attachExtract(...inputs: any[]) { return (this.attachExtract as any)(...inputs); }
attachExtract(...args: any[]) { return this.__attachX(args, "attachExtract"); }
$attachPrepend(...inputs: any[]) { return (this.attachPrepend as any)(...inputs); }
attachPrepend(...args: any[]) { return this.__attachX(args, "attachPrepend"); }
$attachOncePrepend(...inputs: any[]) { return (this.attachOncePrepend as any)(...inputs); }
attachOncePrepend(...args: any[]) { return this.__attachX(args, "attachOncePrepend"); }
$attachOnceExtract(...inputs: any[]) { return (this.attachOnceExtract as any)(...inputs); }
attachOnceExtract(...args: any[]) { return this.__attachX(args, "attachOnceExtract"); }
private __attachX( args: any[], methodName: keyof typeof EvtImpl.propsFormMethodNames ): any {
const propsFromArgs = parsePropsFromArgs<T>(args, "attach*");
const handler = this.addHandler( propsFromArgs, EvtImpl.propsFormMethodNames[methodName] );
return propsFromArgs.timeout === undefined ? this : handler.promise ;
}
postAsyncOnceHandled(data: T): number | Promise<number> {
if (this.isHandled(data)) { return this.post(data); }
const d = new Deferred<number>();
this.evtAttach.attachOnce( ({ op }) => !!invokeOperator(this.getStatelessOp(op), data), () => Promise.resolve().then(() => d.resolve(this.post(data))) );
return d.pr;
}
private postOrPostAndWait(data: T, wait: false): number; private postOrPostAndWait(data: T, wait: true): Promise<void>; private postOrPostAndWait(data: T, wait: boolean): number | Promise<void> {
this.trace(data);
overwriteReadonlyProp(this, "postCount", this.postCount + 1);
//NOTE: Must be before postSync. const postChronologyMark = this.getChronologyMark();
const [isExtracted, prAllHandlerCallbacksResolved] = this.postSync(data);
const getReturnValue = wait ? () => prAllHandlerCallbacksResolved : () => this.postCount;
if (isExtracted) { return getReturnValue(); }
if (this.postAsync === undefined) {
if (this.asyncHandlerCount === 0) { return getReturnValue(); }
this.postAsync = this.postAsyncFactory();
}
this.postAsync(data, postChronologyMark);
return getReturnValue();
}
post(data: T) { return this.postOrPostAndWait(data, false); }
postAndWait(data: T) { return this.postOrPostAndWait(data, true); }
}

//NOTE: For some reason can't set it as static method so we put it hereexport function doDetachIfNeeded<U = any>( handler: Handler<any, U>, opResult: _1.Operator..Result.Matched<U, any>, once: boolean): void;export function doDetachIfNeeded( handler: Handler<any, any>, opResult: _1.Operator..Result.NotMatched<any>,): void;export function doDetachIfNeeded<U = any>( handler: Handler<any, U>, opResult: _1.Operator..Result<U, any>, once?: boolean): void {
const detach = _1.z_f1.fλ_Result_getDetachArg(opResult);
if (typeof detach !== "boolean") { const [ctx, error, res] = detach;
if (!!error) { ctx.abort(error); } else { ctx.done(res); } } else if (detach || !!once) { handler.detach(); }
}
export const Evt: { new <T>(): Evt<T>; readonly prototype: Evt<any>;
readonly create: typeof create;
readonly newCtx: typeof newCtx;
readonly merge: typeof merge;
readonly from: typeof from;
readonly useEffect: typeof useEffect;
readonly getCtx: ReturnType<typeof getCtxFactory>;
readonly loosenType: typeof loosenType;
readonly factorize: typeof factorize;
readonly asPostable: typeof asPostable;
readonly asyncPipe: typeof asyncPipe;
readonly asNonPostable: typeof asNonPostable;
/** https://docs.evt.land/api/evt/setdefaultmaxhandlers */ setDefaultMaxHandlers(n: number): void;
} = EvtImpl;
try { overwriteReadonlyProp(Evt as any, "name", "Evt"); } catch { }
importProxy.Evt = Evt;