I'm new to RxJS and having a mental block. My solution works, but I know it's a hack. What's the proper way to do the following?
// Desired Console Log: {x: 'abcd', y: 'efgh'}
const x = Rx.Observable.of('abcd').delay(1000);
const y = Rx.Observable.of('efgh').delay(1000);
Rx.Observable.concat(x,y)
.scan((acc, cur) => {
if (cur.includes('a')) {
acc.x = cur;
}
if (cur.includes('e')) {
acc.y = cur;
}
return acc
}, {x: false, y: false})
.filter(x => x.y === 'efgh')
.subscribe(x => console.log(x))
// {x: 'abcd', y:'efgh'}
Rx.Observable.concat(x,y)
.zipIterable(['x','y'], (v1, v2) => {
let result = {};
result[v2] = v1;
return result;
})
.toArray()
.map(objects => Object.assign({}, ...objects))
.subscribe(console.log.bind(console, '>> '));
Provided that the order is kept, i.e. the first element is x and the second y
Related
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.
There is one data source that emits values 0 or 1.
If 0 I want to pipe it throught switchMap operator.
If 1 I want to pipe it throught exhaustMap operator.
How can I do that?
https://stackblitz.com/edit/rxjs-gcvsfe
const source = from([0, 0, 0, 1, 1, 1]).pipe(
// if value === 0
switchMap(value => {
return of("Operator switchMap, value: " + value);
})
// if value === 1
// exhaustMap(value => {
// return of('Operator exhaustMap, value: ' + value)
// })
);
source.subscribe(console.log);
If you want to switch operators, then you probably want to have 2 different streams.
If this is the case, then you can use the filter operator and have something like this
const source = from([0, 0, 0, 1, 1, 1]);
const stream_0 = source.pipe(
filter(val => val === 0),
switchMap(value => {
return of("Operator switchMap, value: " + value);
})
)
const stream_1 = source.pipe(
filter(val => val === 1),
exhaustMap(value => {
return of('Operator exhaustMap, value: ' + value)
})
);
merge(stream_0, stream_1).subscribe(console.log);
I end up with this (exhaustMap is inside custom operator paginateList)
this._baseSubject = new Subject<ListPaged>();
this._nextPageSubject = new Subject<ListPaged>();
this._items$ = this._baseSubject.pipe(
switchMap((req) => {
return this._nextPageSubject.pipe(
startWith(req),
paginateList(paginate)
);
})
);
I have an operator that does some recursive operations on a child property of the source. What do I do to merge the child property back into the source after I'm done the recursive operation?
const state = {
posts: [
{id: 3, title: 't1', userId: 1},
],
index: 0,
config: {
previousBufferSize: 1,
nextBufferSize: 1,
}
};
const source = new BehaviorSubject(state);
const generatePreviousPosts$ = (posts) => {
return Observable.create(observer => {
getPost(posts[0].id - 1)
.then(previousPost => {
observer.next([previousPost, ...posts]);
});
});
};
const previousBuffer$ = source.pipe(
pluck('posts'),
expand(generatePreviousPosts$),
tap(console.log),
// What do I do to merge post back in the state so I could use takeWhile?
takeWhile(state => {
const {posts, config, index} = state;
return posts.length <= config.previousBufferSize - index + posts.length &&
posts[0].id != null;
})
);
One way to do it is to use mergeMap, but I feel like there could be a more elegant solution to this problem.
const previousBuffer$ = source.pipe(
mergeMap(
state => (of(state.posts)
.pipe(expand(generatePreviousPosts$))),
(state, posts) => ({...state, posts})),
takeWhile(state => {
const {posts, config, index} = state;
return posts.length <= config.previousBufferSize - index + posts.length &&
posts[0].id != null;
})
);
A much more elegant solution given by https://github.com/Dorus on the rxjs gitter.
const shouldGetPost = ({posts, config, index}) => posts.length <= config.previousBufferSize - index + posts.length
&& posts[0].id != null
const generatePreviousPosts = ({posts, config, index}) => !shouldGetPost({posts, config, index}) ? EMPTY :
from(getPost(posts[0].id - 1)).pipe(
map(previousPost => ({[previousPost, ...posts], config, index}))
)
const previousBuffer$ = source.pipe(
expand(generatePreviousPosts)
);
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
I have data that looks like this:
var records = [
{id: '1', cat: 'A'},
{id: '2', cat: 'A'},
{id: '3', cat: 'B'},
{id: '4', cat: 'B'},
{id: '5', cat: 'B'},
{id: '6', cat: 'C'}
];
I want to create a dc.numberDisplay that displays the count of the number of unique categories, 3 in the example data above (A, B, & C).
This is what I'm currently doing:
var ndx = crossfilter(data); // init crossfilter
// create dimension based on category
var categoryDimension = ndx.dimension(
function (d) {
return d.category;
}
);
// Group by category
var categoryGroup = categoryDimension.group();
var categoryCount = dc.numberDisplay('#category-count'); // An empty span
categoryCount
.group(categoryGroup)
.valueAccessor(
function (d) { return d.value; }
);
The problem is that the numberDisplay displays 2 instead of 3. When debugging, I found that when the valueAccessor is called, d is the count of the number of elements of category A instead of the count of the number of categories.
How can I solve this problem?
UPDATE: Thanks to Nathan's solution, here is a working code snippet (ES2016 style)
const categoryDimension = claims.dimension(
(d) => {
return d.cat;
}
);
const categoryGroup = categoryDimension.groupAll().reduce(
(p, v) => { // add element
const cat = v.cat;
const count = p.categories.get(cat) || 0;
p.categories.set(cat, count + 1);
return p;
},
(p, v) => { // remove element
const cat = v.cat;
const count = p.categories.get(cat);
if (count === 1) {
p.categories.delete(cat);
} else {
p.categories.set(cat, count - 1);
}
return p;
},
() => { // init
return {
categories: new Map()
};
});
categoryCount
.group(categoryGroup)
.valueAccessor(
(d) => {
return d.categories.size;
}
);
You will need to use groupAll() since the number-display only looks at the top group. Then provide custom reduce functions to track unique categories. Finally, when DC.js pulls the value from the top group (there is only one) - just return the number of categories (which is the number of keys in the p object).
var categoryGroup = categoryDimension.groupAll().reduce(
function (p, v) { //add
if(p[v.cat]) {
p[v.cat]++;
} else {
p[v.cat] = 1;
}
return p;
},
function (p, v) { //remove
p[v.cat]--;
if(p[v.cat] === 0) {
delete p[v.cat];
}
return p;
},
function () { //init
//initial p - only one since using groupAll
return {};
}
);
console.debug("groups", categoryGroup.value());
dc.numberDisplay('#category-count')
.group(categoryGroup)
.valueAccessor(
function (d) { return Object.keys(d).length; }
);