I am trying RxJS.
My use case is to parse a log file and group lines by topic ( i.e.: the beginning of the group is the filename and then after that I have some lines with user, date/time and so on)
I can analyse the lines using regExp. I can determine the beginning of the group.
I use ".scan" to group the lines together, when I've the beginning of new group of line, I create an observer on the lines I've accumulated ... fine.
The issue is the end of the file. I've started a new group, I am accumulating lines but I can not trigger the last sequence as I do not have the information that the end. I would have expect to have the information in the complete (but not)
Here is an example using number. Begin of group can multi of 3 or 5. (remark: I work in typescript)
import * as Rx from "rx";
let r = Rx.Observable
.range(0, 8)
.scan( function(acc: number[], value: number): number[]{
if (( value % 3 === 0) || ( value % 5 === 0)) {
acc.push(value);
let info = acc.join(".");
Rx.Observable
.fromArray(acc)
.subscribe( (value) => {
console.log(info, "=>", value);
});
acc = [];
} else {
acc.push(value);
}
return acc;
}, [])
.subscribe( function (x) {
// console.log(x);
});
This emit:
0 => 0
1.2.3 => 1
1.2.3 => 2
1.2.3 => 3
4.5 => 4
4.5 => 5
6 => 6
I am looking how to emit
0 => 0
1.2.3 => 1
1.2.3 => 2
1.2.3 => 3
4.5 => 4
4.5 => 5
6 => 6
7.8 => 7 last items are missing as I do not know how to detect end
7.8 => 8
Can you help me, grouping items?
Any good idea, even not using scan, is welcome.
Thank in advance
You can use the materialize operator. See the documentation here and the marbles here, and an example of use from SO.
In your case, I would try something like (untested but hopefully you can complete it yourself, note that I don't know a thing about typescript so there might be some syntax errors):
import * as Rx from "rx";
let r = Rx.Observable
.range(0, 8)
.materialize()
.scan( function(acc: number[], materializedNumber: Rx.Notification<number>): number[]{
let rangeValue: number = materializedNumber.value;
if (( rangeValue % 3 === 0) || ( rangeValue % 5 === 0)) {
acc.push(rangeValue);
generateNewObserverOnGroupOf(acc);
acc = [];
} else if ( materializedNumber.kind === "C") {
generateNewObserverOnGroupOf(acc);
acc = [];
} else {
acc.push(rangeValue);
}
return acc;
}, [])
// .dematerialize()
.subscribe( function (x) {
// console.log(x);
});
function generateNewObserverOnGroupOf(acc: number[]) {
let info = acc.join(".");
Rx.Observable
.fromArray(acc)
.subscribe( (value) => {
console.log(info, "=>", value);
});
The idea is that the materialize and dematerialize works with notifications, which encodes whether the message being passed by the stream is one of next, error, completed kinds (respectively 'N', 'E', 'C' values for the kind property). If you have a next notification, then the value passed is in the value field of the notification object. Note that you need to dematerialize to return to the normal behaviour of the stream so it can complete and free resources when finished.
Related
I have a costly server ajax request which has one input (full: boolean). If full is false, the server can return either a partial or a full response (response.isFull == true); but if full is true, the server will return a full response. Normally the partial response is good enough, but there are certain conditions that will require a full response. I need to avoid requesting a full response explicitly as much as possible, so I thought I'd start with a BehaviorSubject which I can eventually feed with true and combine it with distinctUntilChanged if I ever need to get the full response. This will give me an observable with false initially and that can give me true if I feed that into it:
const fullSubject = new BehaviorSubject<boolean>(false);
Then I've got a function that takes a boolean parameter and returns an observable with the server request (retried, transformed, etc.). As said, the answer can be partial or full, but it can be full even if the input parameter was false at the server's discretion. For example:
interface IdentityData {
...
isFull: boolean;
}
private getSimpleIdentity(full: boolean): Observable<IdentityData> {
return Axios.get(`/api/identity${full?"?full=true":""}`)
.pipe( ... retry logic ...,
... transformation logic ...,
shareReplay(1) );
}
I need to know how can I combine these so that the following is true:
The server needs to be queried at most twice.
If the first answer is a full answer, no further queries must be performed to the server.
If the first answer is a partial answer, and true is fed into fullSubject, a full answer must be requested.
The expected output from all this is an observable that emits either one full response, or a partial response and, when asked, a full response.
Environment: Vue 2.6.11, RxJS 6.5.5, Axios 0.19.2, TypeScript 3.7.5.
Thanks in advance
Here would be my approach:
const fullSubject = new BehaviorSubject(false);
const src$ = fullSubject.pipe(
switchMap(isFull => Axios.get('...')),
take(2), // Server required at most twice
takeWhile(response => !response.isFull, true), // When `isFull`, it will complete & unsubscribe -> no more requests to the server
shareReplay(1),
);
src$.subscribe(() => { /* ... */ });
function getFullAnswer () {
fullSubject.next(true);
}
takeWhile takes a second argument, inclusive. When set to true, when the predicate function evaluates to false(e.g isFull is true) it will send that value as well. –
if I've got it correctly
private getSimpleIdentity(): Observable<IdentityData> {
return fullSubject.pipe(
switchMap(full => Axios.get(`/api/identity${full ? "?full=true" : ""}`)),
shareReplay(1),
);
}
Uses the retryWhen() operator
const source = of("").pipe(map(() => Math.floor(Math.random() * 10 + 1)));
const example = source
.pipe(
tap((val) => console.log("tap", val)),
map((val) => {
//error will be picked up by retryWhen
if (val !== 5) throw val;
return val;
}),
retryWhen((errors) =>
errors.pipe(
tap(() => console.log("--Wait 1 seconds then repeat")),
delay(1000)
)
)
)
.subscribe((val) => console.log("subscription", val));
/*
output:
tap 3
--Wait 1 seconds then repeat
tap 8
--Wait 1 seconds then repeat
tap 1
--Wait 1 seconds then repeat
tap 4
--Wait 1 seconds then repeat
tap 7
--Wait 1 seconds then repeat
tap 5
subscription 5
*/
I have an endless stream of events and I need to limit them to 5, keep the rest paused for 3 seconds
So need to make a delay after every 5 calls
from([ 1,2,3,4,5,6,7,8,9,11,12,13,14,15,16,17,18,19,21,22,23,24,25,26,27,28 ])
.pipe(
// To demonstrate 1 after 1 values stream we use concatMap
// we return new Observalbe via of operator
// we pipe the delay for each element based on the index value we passed
// in our concatMap
concatMap((x,i) => of(x).pipe(
delayWhen((x) => {
console.log("im index: " + i);
// Not the first element, and every 5th element
return i !== 0 && i % 5 === 0 ? timer(3000): timer(0)})
))
)
.subscribe(x => console.log(x))
// Output: 1,2,3,4,5 ...delay 3s.... 6,7,8,9,10 ...delay 3s...
You can see in this stackblitz I made.
const stream = range(0, 100) // create dataset
.pipe(
bufferCount(5), // slice data into chunks
concatMap( // get this chunk
(msg) => of(msg).pipe(
delay(3000) // and emit every three seconds
))
)
stream.subscribe(item => console.log(item));
So say I have these 3 observables:
const numpad1$ = fromEvent(document, 'keydown')
.pipe(
startWith(0),
filter(({ key, code }) => key === '1' && code === 'Numpad1'),
map(() => 1),
scan((a, b) => a + b, 0),
);
const numpad2$ = fromEvent(document, 'keydown')
.pipe(
startWith(0),
filter(({ key, code }) => key === '2' && code === 'Numpad2'),
map(() => 1),
scan((a, b) => a + b),
);
const numpad3$ = fromEvent(document, 'keydown')
.pipe(
startWith(0),
filter(({ key, code }) => key === '3' && code === 'Numpad3'),
map(() => 1),
scan((a, b) => a + b),
);
Then I use combineLatest like this:
combineLatest(numpad1$, numpad2$, numpad3$).subscribe(console.log)
The subscription doesn't do a console log until numpad 1, 2 and 3 all have been pressed atleast once.
Is there an rxjs operator I can use to have an observable emit whenever any of the inner obersvables have emitted?
For example, in the example above, i'm expecting the console.log to be called when any numpad 1 is pressed for the first time, while numpad 2 and 3 haven't been pressed at all. So the result should be like this: [1, 0, 0] (0s because i'm using startWith(0)).
The solution was actually to just move the startWith(0) to the end of the pipeline.
so:
const numpad1$ = fromEvent(document, 'keydown')
.pipe(
filter(({ key, code }) => key === '1' && code === 'Numpad1'),
map(() => 1),
scan((a, b) => a + b, 0),
startWith(0),
);
The startsWith is filtered out by the filter operator, I suggest using a map function here. For instance:
const numpad3$ = fromEvent(document, 'keydown')
.pipe(
startWith(0),
map(({ key, code }) => return key === '3' && code === 'Numpad3' ? 1 : 0)
);
This will return one if it meets the conditions and 0 else.
I have the following, and it does work, it keeps increasing the delay and eventually timing out which is what I wanted.
But because I am using Concatmap i lose the original value from the interval.
let x = 1
let source2$ = interval(500)
.pipe(
concatMap(() => {
x++
let newtime = x * 500
console.log("newtime ", newtime)
return of(5).pipe(delay(newtime))
}),
timeout(3000),
map((data) => {
return 'Source 2: ' + data
})
)
so it prints Source 2: 5.. where as i want it to print the value of the interval.
I got working what i wanted using the concatmap but i think its the wrong operator as I lose the original value.
Can somebody help?
More info
TO summarize, all i would like to do is emit values using the interval and after each emit increase the delay time - eventually it hits the timeout of 3000 ms and errors out.
I've mentioned in comments that you can use concatMap for this that receives ever increasing index from interval:
concatMap(index => {
let newtime = index * 500
console.log("newtime ", newtime)
return of(index).pipe(delay(newtime))
}),
Notice, that I'm returning the value back to the stream by of(index).
I think I understand what were you concerned about returning another Observable. Since you want to emit items in sequence (emit one only after the previous one completes) then you have to use concatMap with another inner Observable. There isn't a special operator only for this functionality because this is "composable behavior" which means you can achieve this behavior by combining existing operators.
const source2$ = interval(500)
.pipe(
map(x => x * 500),
switchMap(x => timer(x)),
timeout(3000),
map(data => 'Source 2: ' + data)
)
UPDATE:
https://stackblitz.com/edit/rxjs-iywcm6?devtoolsheight=60
const source2$ = interval(500)
.pipe(
tap(x => console.log('Tick before delay', x)),
concatMap(x => timer((x + 1) * 500).pipe(mapTo(x))),
tap(x => console.log('Tick after delay', x)),
map(data => 'Source 2: ' + data),
timeout(3000)
).subscribe(
(data) => console.log(data),
e => console.error('Timeout', e))
I am studying
operator map v.s. flatmap
how to add promise into observable chain.
Then I constructed 4 different versions of var source as below.
version 1, 3 works as expected, while version 2, 4 fail oddly.
My code has also been added in => js bin
Could someone tell what is wrong with my code?
Thanks,
Xi
console.clear();
var p = new Promise((resolve, reject) => {
setTimeout( () => {
resolve('resolved!');
} , 1000);
});
var source = Rx.Observable.interval(200).take(3)
.flatMap(x => Rx.Observable.timer(500).map(() => x)) //version 1, works OK
// .flatMap(x => Rx.Observable.timer(500).map((x) => x)) // version 2, not OK, returns => 0, 0, 0
// .map(x => p.then( s => console.log(s))); // version 3, works OK
// .flatMap(x => p.then( s => console.log(s))); // version 4, not OK, error occurs
source.subscribe(x => console.log(x.toString()));
.flatMap(x => Rx.Observable.timer(500).map((x) => x))
returns "0", "0", "0" because timer emits 0 after 500 ms and map takes that value as input x and returns it with (x) => x. In the previous line, x was not redeclared in the map, so it came from flatMap.
.flatMap(x => p.then( s => console.log(s)));
gives an error because a promise emits the return value of the then callback. That's console.log(s) which being a statement evaluates to undefined. So the flatMap gives an Observable of undefined, undefined, undefined. When the first reaches the subscribe it tries to do undefined.toString and errors out.