Uses the previous matched event data transformation as input à la Array.prototype.reduce
Operators do not have to be pure, they can use variables available in scope and involve time (Date.now()), but they must not have any side effect. In particular they cannot modify their input.
Operator - Filter
Let us consider the example use of an operator that filters out every word that does not start with 'H'.
import { Evt } from"evt";constevtText=Evt.create<string>();evtText.attach( text=>text.startsWith("H"), text=> {console.assert( text.startsWith("H") );console.log(text); });//Nothing will be printed to the console.evtText.post("Bonjour");//"Hi!" will be printed to the console.evtText.post("Hi!");
It is important to be sure that your filter always return a boolean, typewise you will be warned it is not the case but you must be sure that it is actually the case at runtime.
If in doubts use 'bang bang' ( !!returnedValue ). This note also applies for Type Gard operators.
Operator - Type guard
If you use a filter that is also a type guard, the type of the callback argument will be narrowed down to the matched type.
Let us define a straight forward type hierarchy to illustrate this feature.
The matchCircle type guard can be used to attach a callback to an Evt<Shape> that will only be called against circles.
import { Evt } from"evt";constevtShape=Evt.create<Shape>();evtShape.attach( matchCircle, shape =>console.log(shape.radius));//Nothing will be printed on the console, a Square is not a Circle.evtShape.post({ "type":"SQUARE","sideLength":3 });//"33" Will be printed to the console.evtShape.post({ "type":"CIRCLE","radius":33 });
The type of the Shape object is narrowed down to Circle
[ U, "DETACH" ] / [ U, {DETACH:Ctx, ...} ] If the event should be handled AND some detach be performed.
Stateless fλ
Stateless fλ operator only takes the event data as arguments.
import { Evt } from"evt";constevtShape=Evt.create<Shape>();/* * Filter: * Only circle events are handled. * AND * to be handled circles must have a radius greater than 100 * * Transform: * Pass the radius of such circles to the callback. */evtShape.$attach( shape =>shape.type ==="CIRCLE"&&shape.radius >100? [ shape.radius ] :null, radiusOfBigCircle =>console.log(`radius: ${radius}`) //NOTE: The radius argument is inferred as being of type number!);//Nothing will be printed to the console, it's not a circleevtShape.post({ "type":"SQUARE","sideLength":3 }); //Nothing will be printed to the console, The circle is too small.evtShape.post({ "type":"CIRCLE","radius":3 }); //"radius 200" Will be printed to the console.evtShape.post({ "type":"CIRCLE","radius":200 });
Other example using "DETACH"
import { Evt } from"evt";constevtText=Evt.create<"TICK"|"END">();/* * Only handle events that are not "END". * If the event is "END", detach the handler. * Pass the event data string in lower case to the callback. */evtText.$attach( text => text !=="END"? [ text.toLowerCase() ] :"DETACH", text =>console.log(text) );evtText.post("TICK"); //"tick" is printed to the consoleevtText.post("END"); //Nothing is printed on the console, the handler is detachedevtText.post("TICK"); //Nothing is printed to the console.
Example use of [U,null|"DETACH"], handling the event that causes the handler to be detached.
constevtText=Evt.create<"TICK"|"END">();evtText.$attach( text => [ text, text ==="END"?"DETACH":null ], text =>console.log(text) );evtText.post("TICK"); //"TICK" is printed to the consoleevtText.post("END"); //"END" is printed on the console, the handler is detached.evtText.post("TICK"); //Nothing is printed to the console the handler has been detached.
Example use of { DETACH:Ctx}, detaching a group of handlers bound to a given context.
constevtBtnClick=Evt.create<"OK"|"QUIT">();constevtMessage=Evt.create<string>();constevtNotification=Evt.create<string>();constctx=Evt.newCtx();evtMessage.attach( ctx, message =>console.log(`message: ${message}`));evtNotification.attach( ctx, notification =>console.log(`notification: ${notification}`));evtBtnClick.$attach( type => [ type, type !=="QUIT"?null: { "DETACH": ctx } ], type =>console.log(`Button clicked: ${type}`));evtBtnClick.post("OK"); //Prints "Button clicked: OK"evtMessage.post("Hello World"); //Prints "Message: Hello World"evtNotification.post("Poke"); //Prints "Notification: Poke"evtBtnClick.post("QUIT"); //Prints "Button clicked: QUIT", handlers are detached...evtMessage.post("Hello World 2"); //Prints nothingevtNotification.post("Poke 2"); //Prints nothingevtBtnClick.post("OK"); //Prints "Button clicked: OK", evtBtnClick handler hasn't been detached as it was not bound to ctx.
Operators cannot have any side effect (they cannot modify anything). No assumption should be made on when and how they are called.
Don't encapsulate state, do use stateful fλ
The first thing that you might be tempted to do is to use a variable available in the operator's scope as an accumulator.
The following example seems equivalent from the previous one but it is not.
constevtText=Evt.create<string>();//🚨 DO NOT do that 🚨...evtText.$attach( (()=> {let acc="START:";return (data:string) => [acc +=` ${data}`] asconst; })(), sentence =>console.log(sentence));consttext="Foo bar";if( evtText.isHandled(text) ){//Prints "START: Foo Bar Foo bar", probably not what you wanted...evtText.post(text); }
When evt.isHandled(data) is invoked the operator of every handler is invoked. The operator is invoked again when the event is actually posted.
In the example every time the operator is invoked the encapsulated variable acc is updated. This result in "Foo bar" being accumulated twice when the event is posted only once.
evt.postAsyncOnceHandled(data) will also cause dry invokations of the operators.
If state is needed stat full fλ have to be used.
Don't modify input, do return a copy.
import { Evt } from"evt";constevtText=Evt.create<string>();//Do not modify the accumulator value.evtText.$attach( [ (text, arr:string[])=> {arr.push(text);return [arr]; }, [] ], arr=> { /*...*/ });/* ----------------------------- *///Do Return a new arrayevtText.$attach( [ (text, arr:string[]) => [[...arr, text]], [] ], arr=> { /*...*/ });
Do use const assertions ( as const )
The TypeScript const assertion features come in handy if you introduce closures, for example. The following example does not compile without the use of as const.
Generally const assertions can help you narrow down the return type of your operator. In the following example without the const assertions data is inferred as being string | number , with the const assertions it is "TOO LARGE" | number
import { Evt } from"evt";constevtN=Evt.create<number>();evtN.$attach( n => [ n>43?"TOO LARGE"asconst: n ], data=> { /* ... */ });
Do write single instruction function, try to avoid explicit return.
This is more a guideline than a requirement but you should favor data => expression over data=> { ...return x; } wherever possible for multiple reasons:
It is much less likely to inadvertently produce a side effect writing a single expression function than it is writing a function with explicit returns.
Operators are meant to be easily readable. If you think the operator you need is too complex to be clearly expressed by a single instruction, you should consider splitting it in multiple operators and using the compose function introduced in the next section.
It is easier for TypeScript to infer the return type of single expression functions.
Here is the previous example using explicit returns just to show you that the return type has to be explicitly specified, this code does not copy without it.
import { Evt } from"evt";constevtN=Evt.create<number>();//🚨 This is NOT recomanded 🚨...evtN.$attach( (n): [ "TOO LARGE"|number ] => {if( n >43 ){return [ "TOO LARGE" ]; }return [n]; }, data=> { /* ... */ });.
compose(op1, op2, ..., opn)
opn∘...∘op2∘op1
Operators can be composed ( aka piped ) to achieve more complex behaviour.
For most use cases, it is more convenient to chain evt.pipe() calls rather than using compose. However it is very useful for creating custom operators.
Using stateful fλ operators to implement throttleTime(duration), an operator that let through at most one event every duration milliseconds.
import { Evt, compose } from"evt";constthrottleTime= <T>(duration:number) =>compose<T, { data:T; lastClick:number; },T>( [ (data, { lastClick }) =>Date.now() - lastClick < duration ?null: [{ data,"lastClick":Date.now() }], { "lastClick":0,"data":nullasany } ], ({ data }) => [data] ) ;constevtText=Evt.create<string>();evtText.$attach(throttleTime(1000),//<= At most one event per second is handled. text =>console.log(text));setTimeout(()=>evtText.post("A"),0); //Prints "A"//Prints nothing, the previous event was handled less than 1 second ago.setTimeout(()=>evtText.post("B"),500);//Prints nothing, the previous event was handled less than 1 second ago.setTimeout(()=>evtText.post("B"),750); setTimeout(()=>evtText.post("C"),1001); //Prints "C"setTimeout(()=>evtText.post("D"),2500); //Prints "D"
The Operator type alias defines what functions qualify as a valid EVT operaor. The type can be used as a scaffolder to write fλ.
In Operator<T, U> , T design the type of the event data and U design the type of the data spitted out by the operator. For filters operator U=T.
importtype { Operator } from"evt";//A function that take an EVT operator as argument.declarefunctionf<T,U>(op:Operator<T,U>):void;//Les's say you know you want to create an operator that take string//and spit out number you can use the type alias as scaffolding.constmyStatelessFλOp:Operator.fλ<string,number> = str =>str.startsWith("H")?null: [ str.length ];//The shape argument is inferred as being a string and TS control that you//are returning a number (str.length) as you should.f(myStatelessFλOp); //OK, f<Shape,number> Operator.fλ is assignable to Operator.//An other example creating an stateful operatorconstmyStatefulFλOp:Operator.fλ<string,number> = [ (data, prev) => [prev +data.length],0 ];f(myStatefulFλOp); //OK, f<string, number>//Filter and TypeGuard don't need scaffolding but they are valid Operatorf((data:string) =>data.startsWith("H")); // OK, TS infer f<string, string>f((n:number): n is0|1=> n ===0|| n ===1); // OK, TS infer f<number, 0 | 1>
Generic operators such as bufferTimedebounceTime, skip, take, switchMap, mergeMap and reduceWill be added later on alongside creators. To implement those we need a third type of operator called AutonomousOperators that will ship in the next major release.
Some generic operators are provided in "evt/lib/util/genericOperators" such as scan, throttleTime or to but that's about it.
//Importing custom operator chunksOf that is not exported by default.import { chuncksOf } from"evt/lib/util/genericOperators";