I have a bit of code that looks like this:
function getEvensSquared(array) {
return rxjs.of(array).pipe(
rxjs.operators.flatMap(array => {
return rxjs.from(
array.filter(n => n % 2 === 0)
);
}),
rxjs.operators.switchMap(n => {
return rxjs.of(n * n);
}),
rxjs.operators.scan((acc, cur) => {
acc.push(cur);
return acc;
}, [])
);
}
getEvensSquared([1, 2, 3, 4]).subscribe(v => {
console.log("1,2,3,4");
console.log(v)
});
getEvensSquared([1, 3, 5]).subscribe(v => {
console.log("1,3,5"); //never prints
console.log(v)
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/6.2.2/rxjs.umd.min.js"></script>
Essentially it is:
Get a stream of arrays of ObjectA.
Convert the array in to a stream of filtered ObjectA.
Convert the stream of ObjectA into a stream of ObjectB.
Accumulate the stream of ObjectB into a stream of arrays of ObjectB.
The problem is - if there are no approved bookings, the flatMap operator never emits. Is there a way to default in this case?
I tried putting defaultIfEmpty([]) after the scan - but that didn't work.
Positing this as an answer - because it works for the example I've given - defaultIfEmpty does work in this scenario.
function getEvensSquared(array) {
return rxjs.of(array).pipe(
rxjs.operators.flatMap(array => {
return rxjs.from(
array.filter(n => n % 2 === 0)
);
}),
rxjs.operators.switchMap(n => {
return rxjs.of(n * n);
}),
rxjs.operators.scan((acc, cur) => {
acc.push(cur);
return acc;
}, []),
rxjs.operators.defaultIfEmpty([])
);
}
getEvensSquared([1, 2, 3, 4]).subscribe(v => {
console.log("1,2,3,4");
console.log(v)
});
getEvensSquared([1, 3, 5]).subscribe(v => {
console.log("1,3,5"); //works
console.log(v)
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/6.2.2/rxjs.umd.min.js"></script>
Related
I have observable [1,1,1,2,2,1,1], and want to convert it in observable of sums of same elements group, so result would be [3,4,2]. I wrote this code:
from([1, 1, 1, 2, 2, 1, 1])
.pipe(
connect((numbers$) =>
numbers$.pipe(window(checkChange(numbers$)), mergeMap(sumNumbers))
)
)
.subscribe(console.log);
function checkChange(obs$: Observable<number>): Observable<any> {
return obs$.pipe(
pairwise(),
filter(([a, b]) => a !== b),
map(() => "change")
);
}
function sumNumbers(obs$: Observable<number>): Observable<number> {
return obs$.pipe(reduce((acc, n) => acc + n, 0));
}
But it returns observable [5,3,1]. This is because original observable windows after element where change happens, and not before. So it groups by [[1,1,1,2],[2,1],[1]], and not [[1,1,1],[2,2],[1,1]]. How can I fix it?
You could achieve it using other notifier observable that runs before the window observable gets processed.
You could do something like this
const source = from([1, 1, 1, 2, 2, 1, 1]);
const myNotifier = new Subject<number>(); //Subject to be used as notifier
source
.pipe(
connect((shared$) =>
merge(
shared$.pipe(notifyOnChange(myNotifier)), // 1st run the notifier stream
shared$.pipe(window(myNotifier)) // 2nd the windowed one.
)
),
mergeMap(sumNumbers)
)
.subscribe(console.log);
function notifyOnChange<T>(notifier: Subject<T>) {
return (obs$: Observable<T>): Observable<never> =>
obs$.pipe(
distinctUntilChanged(), // only lets through the value if different to previous one
skip(1), // skip the first emission
tap(notifier), // notify the change
ignoreElements() //prevent the next notification propagation
);
}
function sumNumbers(obs$: Observable<number>): Observable<number> {
return obs$.pipe(reduce((acc, n) => acc + n, 0));
}
Cheers
I would use reduce instead to maintain the current value being counted, running count of correct value, and an array of previous counts.
import {from} from 'rxjs';
import {reduce,map} from 'rxjsoperators';
from([1, 1, 1, 2, 2, 1, 1]).pipe(
reduce(
({ prevCounts, prev, count }, cur) => (count === undefined) ? {
prevCounts,
prev : cur,
count : 1
} : (prev === cur) ? {
prevCounts,
prev,
count: count + 1
} : {
prevCounts: [...prevCounts, count],
prev: cur,
count: 1
},
{ prevCounts : [] }
),
// append count to prevCounts array
map(({ prevCounts, count }) => (count) ? [...prevCounts, count] : prevCounts)
).subscribe({
next: x => console.log(x)
});
This code doesn't use array.push because that changes the array in place and while in this case it wouldn't be a bad thing I've learned it's safer to have the functional programming habit of either returning unchanged objects or new objects.
I'm trying to write a reactive function with rxjs that, given a potentially infinite array:
Rule 1: Skip initial null items
Rule 2: Extract the items between two '*' appearances
Rule 3: If first item after nulls is not an '*', must fail (or return an empty array)
Rule 4: Process no more than N items
Rule 5: If there's no a second '*', must fail (or return an empty array)
So, with N = 10:
Case 1: [null, null, '*', 1, 2, 3, '*', 4, 5] -> [1, 2, 3]
Case 2: [null, null, 1, '*', 2, 3, '*', 4, 5] -> [] // Breaks rule 3
Case 3: [null, null, '*', 1, 2, 3, 4, 5, 6, 7, 8, '*'] -> [] // Breaks rule 5 (second * is at position > N)
For the case 1, there's no problem. But I don't find the set of operator to enforce the rules 3 and 5
This example illustrates the problem:
const { from } = require('rxjs');
const { take, takeWhile, skipWhile, toArray } = require('rxjs/operators');
function *infinite(items) {
for (let i = 0; ; i++) {
yield i < items.length ? items[i] : `fake${i}`
}
}
const extract = ({
source,
limit = 10,
}) => new Promise(resolve => {
source
.pipe(...[
take(limit),
skipWhile(item => item === null),
skipWhile(item => item === '*'),
takeWhile(item => item !== '*'),
toArray(),
])
.subscribe(result => {
resolve(result)
})
})
;(async () => {
console.log(await extract({ source: from(infinite([null, '*', 1, 2, 3, '*', 4, 5, 6])) }))
console.log(await extract({ source: from(infinite([null, 'a', '*', 1, 2, 3, '*', 4, 5, 6])) }))
console.log(await extract({ source: from(infinite([null, '*', 1, 2, 3, 4, 5, 6, 8, 9, 10, 11, 12])) }))
})()
Edit: I realized the operation:
skipWhile(item => item === '*'),
is not accurate. Should be something like
skipThisSingleItemIfMatchAsteriskOtherwiseFail
A possible solution for your problem is the following. Comments are inline
function extract(c: any[], n: number) {
// first you create an src stream where all the leading nulls are removed
const src = from(c).pipe(
// just take the first n elements
take(n),
// filter to remove the nulls - this can be a problem if you have nulls between the 2 '*'
filter((item) => item !== null),
// share is used to avoid having more than one subscription to this stream
share()
);
const core = src.pipe(
// here we consider all elements until the second '*' is met
// And what about the first '*'? see the rest of the code, there is the explanation
takeWhile((item) => item !== "*", true),
// create an array which cumulates all the values received until the stream completes
toArray(),
// if the array of all elements received is empty or if the last element is not a '*' return []
// else return the elements received a part the last '*'
map((arr) => {
return arr.length === 0
? []
: arr[arr.length - 1] !== "*"
? []
: arr.slice(0, arr.length - 1);
})
);
// this is the stream returned by the extract function
// it starts from the src stream we have created above
return src.pipe(
// the first element is taken
// since the src stream is shared between this stream and the stream we have called "core" and have built above
// then it means that the first element is consumed here and will not be encountered in the "core" stream
first(),
// if the first element is not a '*' an error is thrown
tap((d) => {
if (d !== "*") {
throw new Error("First not null val is not *");
}
}),
// if no error is thrown then we return the stream "core"
concatMap((firstItem) => {
return core;
}),
// if an error is thrown then we return an Observable which emits []
catchError((e) => of([]))
);
}
In order to use this function you can write the following code
const resp = extract(source, 10);
resp.subscribe((d) => {
// do stuff with the result, for instance
console.log(d);
});
Here a stackblitz that reproduces this logic
How about something with custom RxJS operators? Try the following
const { Observable, from } = rxjs;
const { take, filter, reduce } = rxjs.operators;
const onlyIfFirst = predicate => {
let first = true;
return source =>
new Observable(subscriber =>
source.subscribe({
next(value) {
if (first) {
first = false;
if (predicate(value)) {
subscriber.next(value);
} else {
subscriber.next([]);
subscriber.complete();
}
} else {
subscriber.next(value);
}
},
complete() {
subscriber.complete();
}
})
);
};
const toArrayWhen = (predicate, count) => {
let id = 0;
let times = count * 2;
return source =>
source.pipe(
reduce((acc, curr) => {
if (!!id) {
if (predicate(curr)) id++;
if (id < times && !predicate(curr)) {
acc = [...acc, curr];
}
} else {
if (predicate(curr)) id++;
}
return acc;
}, [])
);
};
const input = [null, null, '*', 1, 2, 3, '*', 3, '*', 4, '*'];
from(input)
.pipe(
take(10),
filter(value => value !== null),
onlyIfFirst(value => value === '*'),
toArrayWhen(value => value === '*', 1)
)
.subscribe({
next: value => console.log('Next:', value),
error: error => console.log('Error:', error),
complete: () => console.log('Complete')
});
<script src="https://unpkg.com/rxjs#6.2.2/bundles/rxjs.umd.min.js"></script>
Note: I'm fairly certain the count variable behavior for > 1 is buggy at the moment. But as long as you only need the first instead of values between two asterisks *, it should be find.
I have a stream of files and I want to fill additional information about it, but I would like to present the currently obtained data to the user, as it is all that is initially visible anyway.
I want observable that:
Get cancelled on new emission (like switchMap)
Does not wait for the observable to finish before emitting (like tap)
What I have currently is awaiting the result, before emitting the files.
Set-up & current try itteration:
this.pagedFLFiles = fileService.getFiles().pipe(
switchMap(response => concat(
of(response),
fileService.getAdditionalInfo(response.items).pipe(
switchMap(() => EMPTY),
),
)),
shareReplay(1),
);
fileService.getAdditionalInfo(response.items) - it is modifing the data
getAdditionalInfo(files: FLFile[]): Observable<FLFile[]> {
return this.api.getWithToken(token => {
return { path: `v5/user/${token}/files/${files.map(file => file.id).join(',')}}/facilities` };
}).pipe(
map(information => {
files.forEach(file => {
const info = information[file.id];
(Object.entries(info) as [keyof typeof info, any][]).forEach(([key, value]) => {
file[key] = value;
});
});
return files;
}),
);
}
Use merge instead of concat.
Concat waits for both observables, of(reponse) and getAdditionalInfo, before emitting a value.
Merge emits each time one of its observables emits.
Example:
getFiles will emit each second for 3 seconds
getAdditionalInfo will be cancelled 2 times (because it runs longer than 1 seond), and therefore will only modify the last emitted files array
import { merge, EMPTY, timer, of, interval } from 'rxjs';
import { finalize, switchMap, map, take, shareReplay } from 'rxjs/operators';
const fileService = {
getFiles: () => interval(1000).pipe(
take(3),
map(x => {
const items = [0, 1, 2].map(i => { return { 'info1': i }; })
return { 'index': x, 'items': items };
})
),
getAdditionalInfo: (files) => {
let wasModified = false;
return timer(2000).pipe(
map(information => {
files.forEach(file => {
file['info2'] = 'information' + files.length;
});
console.log('getAdditionalInfo: modified data');
wasModified = true;
return files;
}),
finalize(() => {
if (!wasModified) {
console.log('getAdditionalInfo: cancelled');
}
})
);
}
}
const pagedFLFiles = fileService.getFiles().pipe(
switchMap(response => {
return merge(
of(response),
fileService.getAdditionalInfo(response.items).pipe(
switchMap(() => EMPTY),
));
}
),
shareReplay(1),
);
pagedFLFiles.subscribe(x => {
console.log('immediate', x.index);
});
Stackblitz
I am trying to create generic pipe that will catch errors and will affect other pipes.
Our producer:
const producer = () => of(1, 2).pipe(
map((primitiveValue: number) => {
if (primitiveValue === 2) {
return throwError(primitiveValue)
}
return {value: primitiveValue};
}),
);
This pipe will be attached to any producer, and will listen for errors:
const genericDecorator = (observable) =>
observable
.pipe(
catchError((primitiveValue) => {
return of('some error ' + primitiveValue)
})
);
Subscription:
genericDecorator(producer())
.pipe(
map((wrapped) => {
return wrapped.value;
})
)
.subscribe((value) => {
result.push(value)
});
What I am trying to achieve is
result = [1, 'some error 2']
And what I got is
result = [1, undefined]
This seems logical, but is there any way to skip one pipe?
It looks like you could just replace the value returned by catchError:
catchError((primitiveValue) => {
return of({ value: 'some error ' + primitiveValue })
})
Problem: Game: So I have some ships that can arrive to many planets. If the 2 ships arrive at the same time on the new planet can lead to the same process of changing ownership twice. This process is asynchronous and should only happen once per planet ownership change.
To fix this I want split the stream of ships by planet id so each stream will be for only one planet. Now the tricky part is that each ship should only be processed after the previous one has been processed.
Ships$
Split by planet id
planet id1: process in sequence
planet id2: process in sequence
...
Here is some code that will show how it should behave.
const ships = [
{
id: 1,
planetId: 1,
},
{
id: 2,
planetId: 1,
},
{
id: 3,
planetId: 2,
},
// ... never finishes
]
// the source observable never finishes
const source$ = interval(1000).pipe(
take(ships.length),
map(i => ships[i]),
)
const createSubject = (ship) => {
// Doesn't need to be a subject, but needs to emit new items after a bit of time based on some other requests.
console.log(`>>>`, ship.id);
const subject = new Subject();
setTimeout(() => {
subject.next(ship.id + ' a' + new Date());
}, 1000);
setTimeout(() => {
subject.next(ship.id + ' b' + new Date());
subject.complete();
}, 2000);
return subject.asObservable();
}
// The result should be the following (t, is the time in seconds, t3, is time after 3 seconds)
// t0: >>> 1
// t0: >>> 3
// t1: 1 a
// t1: 2 a
// t2: 1 b
// t2: 2 b
// t2: >>> 2 (note that the second ship didn't call the createSubject until the first finished)
// t3: 1 a
// t4: 1 2
Solution (with a lot of help from A.Winnen and some figuring out)
Run it here: https://stackblitz.com/edit/angular-8zopfk?file=src/app/app.component.ts
const ships = [
{
id: 1,
planetId: 1,
},
{
id: 2,
planetId: 1,
},
{
id: 3,
planetId: 2,
}
];
const createSubject = (ship) => {
console.log(ship.id + ' a')
const subject = new Subject();
setTimeout(() => {
//subject.next(ship.id + ' b');
}, 500);//
setTimeout(() => {
subject.next(ship.id + ' c');
subject.complete();//
}, 1000);
return subject.asObservable();
}
let x = 0;
interval(10).pipe(//
take(ships.length),
map(i => ships[i]),
groupBy(s => s.planetId),
mergeMap(group$ => {//
x++
return group$.pipe(
tap(i => console.log('x', i, x)),
concatMap(createSubject)
)
}),
).subscribe(res => console.log('finish', res), undefined, () => console.log("completed"))
How can this be done in rxjs?
Code:
const shipArriveAction$ = action$.pipe<AppAction>(
ofType(ShipActions.arrive),
groupBy(action => action.payload.ship.toPlanetId),
mergeMap((shipByPlanet$: Observable<ShipActions.Arrive>) => {
return shipByPlanet$.pipe(
groupBy(action => action.payload.ship.id),
mergeMap((planet$) => {
return planet$.pipe(
concatMap((action) => {
console.log(`>>>concat`, new Date(), action);
// this code should be called in sequence for each ship with the same planet. I don't need only the results to be in order, but also this to be called in sequence.
const subject = new Subject();
const pushAction: PushAction = (pushedAction) => {
subject.next(pushedAction);
};
onShipArriveAction(state$.value, action, pushAction).then(() => {
subject.complete();
});
return subject.asObservable();
}),
)
})
);
)
;
The code from A.Winnen is very close, but only works with a source observable that is finished, not continuous:
const ships = [
{
id: 1,
planetId: 1,
},
{
id: 2,
planetId: 1,
},
{
id: 3,
planetId: 2,
}
];
const createSubject = (ship) => {
console.log(ship.id + ' a')
const subject = new Subject();
setTimeout(() => {
subject.next(ship.id + ' b');
}, 1000);//
setTimeout(() => {
subject.next(ship.id + ' c');
subject.complete();//
}, 2000);
return subject.asObservable().pipe(
finalize(null)
);
}
interval(1000).pipe(
take(ships.length),
tap(console.log),
map(i => ships[i]),
groupBy(s => s.planetId),
mergeMap(group => group.pipe(toArray())),
mergeMap(group => from(group).pipe(
concatMap(createSubject)
))
).subscribe(res => console.log(res), undefined, () => console.log("completed"))
you can use a combination of groupBy and mergeMap to achieve your goal.
from(ships).pipe(
groupBy(ship => ship.planetId),
mergeMap(planetGroup => planetGroup.pipe(
concatMap(ship => {
// do real processing in this step
return of(`planetGroup: ${planetGroup.key} - processed ${ship.ship}`);
})
))
).subscribe(result => console.log(result));
I made a simple example: https://stackblitz.com/edit/angular-6etaja?file=src%2Fapp%2Fapp.component.ts
EDIT:
updated blitzstack: https://stackblitz.com/edit/angular-y7znvk