I have process function which works in async way - it takes line of input text and process it (in snippet below - for simplification - it extract number from line). The lines came form input large file (many GB) and are read and send to pipe one-by-one (this is 'simulated' by of operator below). The process function is slow - but reading lines is fast - so critical point is to read and process lines one-by-one to avoid memory "stack-overflow".
const { of, Subject } = rxjs;
const { take, bufferCount, map, finalize } = rxjs.operators;
let source = of("line 11 of text", "line 22 of text", "line 33 of text") // large file with lines
.pipe(map(line => process(line, ()=> {
// ???? how te callback/pipe shoud look ????
}))
);
source.subscribe(x=> console.log(x));
// expected result shoud be:
// 11
// 22
// 33
// This is third-party library - I cannot change it
function process(line, callback) {
setTimeout(_=>{
let result = line.match(/\d/)[0];
callback(result);
}, (1+Math.random()*9)*100); // random processing time
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/6.5.5/rxjs.umd.min.js" integrity="sha256-85uCh8dPb35sH3WK435rtYUKALbcvEQFC65eg+raeuc=" crossorigin="anonymous"></script>
I don't know how to do it - any idea?
You need to convert your callback to Promise as Promises can work with rxjs streams.
import { of } from "rxjs";
import { concatMap } from "rxjs/operators";
const promiseWrapper = line => new Promise(resolve => process(line, resolve));
let source = of("line 11 of text", "line 22 of text", "line 33 of text").pipe(
concatMap(item => promiseWrapper(item))
);
source.subscribe(x => console.log(x));
// expected result shoud be:
// 11
// 22
// 33
// This is third-party library - I cannot change it
function process(line, callback) {
setTimeout(_ => {
let result = line.match(/\d+/)[0];
callback(result);
}, 1000);
}
also, I've prepared stackblitz (or below snippet)
const { of, Subject } = rxjs;
const { concatMap } = rxjs.operators;
const promiseWrapper = line => new Promise(resolve => process(line, resolve));
let source = of(
"line 11 of text",
"line 22 of text",
"line 33 of text") // large file with lines
.pipe(concatMap(item => promiseWrapper(item))
);
source.subscribe(x=> console.log(x));
// expected result shoud be:
// 11
// 22
// 33
// This is third-party library - I cannot change it
function process(line, callback) {
setTimeout(_=>{
let result = line.match(/\d+/)[0];
callback(result);
}, (1+Math.random()*9)*100);
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/6.5.5/rxjs.umd.min.js" integrity="sha256-85uCh8dPb35sH3WK435rtYUKALbcvEQFC65eg+raeuc=" crossorigin="anonymous"></script>
You can use bindCallback to convert a function of type f(x, callback) to a function g(x) that returns a cold observable that emits the result passed to the callback.
import { bindCallback, of } from "rxjs";
import { concatMap } from "rxjs/operators";
const boundProcess = bindCallback(process);
let source = of("line 11 of text", "line 22 of text", "line 33 of text").pipe(
concatMap(line => boundProcess(line))
);
source.subscribe(x => console.log(x));
// expected result shoud be:
// 11
// 22
// 33
// This is third-party library - I cannot change it
function process(line, callback) {
setTimeout(_ => {
let result = line.match(/\d+/)[0];
callback(result);
}, 1000);
}
https://stackblitz.com/edit/typescript-2rai4c?file=index.ts
Related
Hellow,
I have below Json structure, which is provided as a payload in the UpdateOrders action.
In the effect, I would like to iterate over the reservations and orders, call the this.orderApiService.updateOrder service and dispatch a UpdateOrderProgress action. In the UpdateOrderProgress action I would like to provide the numberOfReservationsUpdated and the totalReservations
const reservationOrders = [
{
reservationNumber: '22763883',
orders: [
{
orderId: 'O12341',
amount: 25
},
{
orderId: 'O45321',
amount: 50
}
]
},
{
reservationNumber: '42345719',
orders: [
{
orderId: 'O12343',
amount: 75
}
]
}
];
I have the following effect to achieve this, but unfortunately, this effect does not work and throws an exception.
#Effect()
updateOrders$ = this.actions$.pipe(
ofType<UpdateOrders>(UpdateOrdersActionType.UPDATE_ORDERS),
filter((action) => !!action.reservationOrders),
exhaustMap((action) => {
return combineLatest(action.reservationOrders.map((x, index) => {
const totalReservations = action.reservationOrders.length;
const numberOfReservationsUpdated = index + 1;
return combineLatest(x.orders.map((order) => {
const orderUpdateRequest: OrderUpdateRequest = {
orderId: order.orderId,
amount: order.amount
};
return this.orderApiService.updateOrder(orderUpdateRequest).pipe(
switchMap(() => [new UpdateOrderProgress(numberOfReservationsUpdated, totalReservations)]),
catchError((message: string) => of(console.info(message))),
);
}))
}))
})
);
How can I achieve this? Which RxJs operators am I missing?
Instead of using combineLatest, you may switch to using a combination of merge and mergeMap to acheive the effect you're looking for.
Below is a representation of your problem statement -
An action triggers an observable
This needs to trigger multiple observables
Each of those observables need to then trigger some
action (UPDATE_ACTION)
One way to achieve this is as follows -
const subj = new Subject<number[]>();
const getData$ = (index) => {
return of({
index,
value: 'Some value for ' + index,
}).pipe(delay(index*1000));
};
const source = subj.pipe(
filter((x) => !!x),
exhaustMap((records: number[]) => {
const dataRequests = records.map((r) => getData$(r));
return merge(dataRequests);
}),
mergeMap((obs) => obs)
);
source.subscribe(console.log);
subj.next([3,1,1,4]); // Each of the value in array simulates a call to an endpoint that'll take i*1000 ms to complete
// OUTPUT -
// {index: 1, value: "Some value for 1"}
// {index: 1, value: "Some value for 1"}
// {index: 3, value: "Some value for 3"}
// {index: 4, value: "Some value for 4"}
Given the above explaination, your code needs to be changed to something like -
const getOrderRequest$ = (order: OrderUpdateRequest, numberOfReservationsUpdated, totalReservations) => {
const orderUpdateRequest: OrderUpdateRequest = {
orderId: order.orderId,
amount: order.amount
};
return this.orderApiService.updateOrder(orderUpdateRequest).pipe(
switchMap(() => new UpdateOrderProgress(numberOfReservationsUpdated, totalReservations)),
catchError((message: string) => of(console.info(message))),
);
}
updateOrders$ = this.actions$.pipe(
ofType<UpdateOrders>(UpdateOrdersActionType.UPDATE_ORDERS),
filter((action) => !!action.reservationOrders),
exhaustMap((action) => {
const reservationOrders = action.reservationOrders;
const totalLen = reservationOrders.length
const allRequests = []
reservationOrders.forEach((r, index) => {
r.orders.forEach(order => {
const req = getOrderRequest$(order, index + 1, totalLen);
allRequests.push(req);
});
});
return merge(allRequests)
}),
mergeMap(obs=> obs)
);
Side Note - While the nested observables in your example may work, there are chances that you'll be seeing wrong results due to inherent nature of http calls taking unknown amount of time to complete.
Meaning, the way you've written it, there are chances that you can see in some cases that numberOfReservationsUpdated as not an exact indicative of actual number of reservations updated.
A better approach would be to handle the state information in your reducer. Basically, pass the reservationNumber in the UPDATE action payload and let the reducer decide how many requests are pending completion. This will be an accurate representation of the state of the system. Also, it will simplify your logic in #effect to a single nested observable rather than multiple nesting.
I'm using RxJS 6 to lazily step through iterable objects using code similar to example running below. This is working well but I'm having trouble solving my final use case.
Full code here
import { EMPTY, defer, from, of } from "rxjs";
import { delay, expand, mergeMap, repeat } from "rxjs/operators";
function stepIterator (iterator) {
return defer(() => of(iterator.next())).pipe(
mergeMap(result => result.done ? EMPTY : of(result.value))
);
}
function iterateValues ({ params }) {
const { values, delay: delayMilliseconds } = params;
const isIterable = typeof values[Symbol.iterator] === "function";
// Iterable values which are emitted over time are handled manually. Otherwise
// the values are provided to Rx for resolution.
if (isIterable && delayMilliseconds > 0) {
const iterator = values[Symbol.iterator]();
// The first value is emitted immediately, the rest are emitted after time.
return stepIterator(iterator).pipe(
expand(v => stepIterator(iterator).pipe(delay(delayMilliseconds)))
);
} else {
return from(values);
}
}
const options = {
params: {
// Any iterable object is walked manually. Otherwise delegate to `from()`.
values: ["Mary", "had", "a", "little", "lamb"],
// Delay _between_ values.
delay: 350,
// Delay before the stream restarts _after the last value_.
runAgainAfter: 1000,
}
};
iterateValues(options)
// Is not repeating?!
.pipe(repeat(3))
.subscribe(
v => {
console.log(v, Date.now());
},
console.error,
() => {
console.log('Complete');
}
);
I'd like to add in another option which will re-execute the stream, an indefinite number of times, after a delay (runAgainAfter). I'm having trouble composing this in cleanly without factoring the result.done case deeper. So far I've been unable to compose the run-again behavior around iterateValues.
What's the best approach to accomplish the use case?
Thanks!
Edit 1: repeat just hit me in the face. Perhaps it means to be friendly.
Edit 2: No, repeat isn't repeating but the observable is completing. Thanks for any help. I'm confused.
For posterity here is the full code sample for a revised edition is repeat-able and uses a consistent delay between items.
import { concat, EMPTY, defer, from, interval, of, throwError } from "rxjs";
import { delay, expand, mergeMap, repeat } from "rxjs/operators";
function stepIterator(iterator) {
return defer(() => of(iterator.next())).pipe(
mergeMap(result => (result.done ? EMPTY : of(result.value)))
);
}
function iterateValues({ params }) {
const { values, delay: delayMilliseconds, times = 1 } = params;
const isIterable =
values != null && typeof values[Symbol.iterator] === "function";
if (!isIterable) {
return throwError(new Error(`\`${values}\` is not iterable`));
}
// Iterable values which are emitted over time are handled manually. Otherwise
// the values are provided to Rx for resolution.
const observable =
delayMilliseconds > 0
? defer(() => of(values[Symbol.iterator]())).pipe(
mergeMap(iterator =>
stepIterator(iterator).pipe(
expand(v => stepIterator(iterator).pipe(delay(delayMilliseconds)))
)
)
)
: from(values);
return observable.pipe(repeat(times));
}
I'm gonna be honest, but there could be better solution for sure. In my solution, I ended up encapsulating delay logic in a custom runAgainAfter operator. Making it an independent part, that doesn't affect your code logic directly.
Full working code is here
And the code of runAgainAfter if anybody needs it:
import { Observable } from "rxjs";
export const runAgainAfter = delay => observable => {
return new Observable(observer => {
let timeout;
let subscription;
const subscribe = () => {
return observable.subscribe({
next(value) {
observer.next(value);
},
error(err) {
observer.error(err);
},
complete() {
timeout = setTimeout(() => {
subscription = subscribe();
}, delay);
}
});
};
subscription = subscribe();
return () => {
subscription.unsubscribe();
clearTimeout(timeout);
};
});
};
Hope it helps <3
I am trying to test that redux actions are fired when the finish event on my upload object is fired.
Here's my epic:
const uploadFileEpic = (action$, state$, dependencies$) =>
action$.pipe(
ofType(uploadActions.UPLOAD_FILE),
mergeMap(({ payload }) => {
const { file, masterHandle } = payload;
return new Observable(o => {
const upload = masterHandle.uploadFile("/", file);
const handle = upload.handle;
upload.on("finish", () => {
o.next(
uploadActions.uploadSuccess({
masterHandle
})
);
o.complete();
});
});
})
);
Here is what I have written so far. It doesn't work:
import { of } from "rxjs";
import "rxjs/add/operator/toArray";
import { EventEmitter } from "events";
import uploadActions from "../actions/upload-actions";
import uploadEpic from "./upload-epic";
test("uploadFilesEpic filesActions.UPLOAD_FILE on success", done => {
const file = { name: "f1" };
const upload = new EventEmitter();
upload.handle = "h1";
const masterHandle = {
uploadFile: jest.fn(() => upload)
};
const action$ = of(uploadActions.uploadFile({ file, masterHandle }));
upload.emit("finish");
uploadEpic(action$).subscribe(actions => {
expect(actions).toEqual(uploadActions.uploadSuccess({ masterHandle }));
done();
});
});
It says the async callback was not fired:
FAIL src/redux/epics/upload-epic.test.js (8.531s)
✓ uploadFilesEpic filesActions.UPLOAD_FILES (9ms)
✕ uploadFilesEpic filesActions.UPLOAD_FILE on success (5021ms)
● uploadFilesEpic filesActions.UPLOAD_FILE on success
Timeout - Async callback was not invoked within the 5000ms timeout specified by jest.setTimeout.Error:
23 | });
24 |
> 25 | test("uploadFilesEpic filesActions.UPLOAD_FILE on success", done => {
| ^
26 | const file = { name: "f1" };
27 | const upload = new EventEmitter();
28 | upload.handle = "h1";
at new Spec (node_modules/jest-config/node_modules/jest-jasmine2/build/jasmine/Spec.js:116:22)
at Object.test (src/redux/epics/upload-epic.test.js:25:1)
Test Suites: 1 failed, 1 total
Tests: 1 failed, 1 passed, 2 total
Snapshots: 0 total
Time: 9.297s
It makes sense to me that the test is failing but I'm not sure how to subscribe to the epic, fire the finish event, and then inspect the actions returned by the epic.
The event for finishing the upload is emitted too early in the test. The EventEmitter does not keep a buffer of events and if no one is subscribed, then the even is lost.
Move the emit "finish" to the bottom of the test.
upload.emit("finish"); // must emit after subscribing
I want to test an effect that works as follows:
Effect starts if LoadEntriesSucces action was dispatched
It waits for 5 seconds
After 5 seconds passes http request is send
When response arrives, new action is dispatched (depending, whether response was succes or error).
Effect's code looks like this:
#Effect()
continuePollingEntries$ = this.actions$.pipe(
ofType(SubnetBrowserApiActions.SubnetBrowserApiActionTypes.LoadEntriesSucces),
delay(5000),
switchMap(() => {
return this.subnetBrowserService.getSubnetEntries().pipe(
map((entries) => {
return new SubnetBrowserApiActions.LoadEntriesSucces({ entries });
}),
catchError((error) => {
return of(new SubnetBrowserApiActions.LoadEntriesFailure({ error }));
}),
);
}),
);
What I want to test is whether an effect is dispatched after 5 seconds:
it('should dispatch action after 5 seconds', () => {
const entries: SubnetEntry[] = [{
type: 'type',
userText: 'userText',
ipAddress: '0.0.0.0'
}];
const action = new SubnetBrowserApiActions.LoadEntriesSucces({entries});
const completion = new SubnetBrowserApiActions.LoadEntriesSucces({entries});
actions$ = hot('-a', { a: action });
const response = cold('-a', {a: entries});
const expected = cold('- 5s b ', { b: completion });
subnetBrowserService.getSubnetEntries = () => (response);
expect(effects.continuePollingEntries$).toBeObservable(expected);
});
However this test does not work for me. Output from test looks like this:
Expected $.length = 0 to equal 3.
Expected $[0] = undefined to equal Object({ frame: 20, notification: Notification({ kind: 'N', value: undefined, error: undefined, hasValue: true }) }).
Expected $[1] = undefined to equal Object({ frame: 30, notification: Notification({ kind: 'N', value: undefined, error: undefined, hasValue: true }) }).
Expected $[2] = undefined to equal Object({ frame: 50, notification: Notification({ kind: 'N', value: LoadEntriesSucces({ payload: Object({ entries: [ Object({ type: 'type', userText: 'userText', ipAddress: '0.0.0.0' }) ] }), type: '[Subnet Browser API] Load Entries Succes' }), error: undefined, hasValue: true }) }).
What should I do to make this test work?
Like mentioned in another answer, one way to test that effect would be by using the TestScheduler but it can be done in a simpler way.
We can test our asynchronous RxJS code synchronously and deterministically by virtualizing time using the TestScheduler. ASCII marble diagrams provide a visual way for us to represent the behavior of an Observable. We can use them to assert that a particular Observable behaves as expected, as well as to create hot and cold Observables we can use as mocks.
For example, let's unit test the following effect:
effectWithDelay$ = createEffect(() => {
return this.actions$.pipe(
ofType(fromFooActions.doSomething),
delay(5000),
switchMap(({ payload }) => {
const { someData } = payload;
return this.fooService.someMethod(someData).pipe(
map(() => {
return fromFooActions.doSomethingSuccess();
}),
catchError(() => {
return of(fromFooActions.doSomethinfError());
}),
);
}),
);
});
The effect just waits 5 seconds after an initial action, and calls a service which would then dispatch a success or error action. The code to unit test that effect would be the following:
import { TestBed } from "#angular/core/testing";
import { provideMockActions } from "#ngrx/effects/testing";
import { Observable } from "rxjs";
import { TestScheduler } from "rxjs/testing";
import { FooEffects } from "./foo.effects";
import { FooService } from "../services/foo.service";
import * as fromFooActions from "../actions/foo.actions";
// ...
describe("FooEffects", () => {
let actions$: Observable<unknown>;
let testScheduler: TestScheduler; // <-- instance of the test scheduler
let effects: FooEffects;
let fooServiceMock: jasmine.SpyObj<FooService>;
beforeEach(() => {
// Initialize the TestScheduler instance passing a function to
// compare if two objects are equal
testScheduler = new TestScheduler((actual, expected) => {
expect(actual).toEqual(expected);
});
TestBed.configureTestingModule({
imports: [],
providers: [
FooEffects,
provideMockActions(() => actions$),
// Mock the service so that we can test if it was called
// and if the right data was sent
{
provide: FooService,
useValue: jasmine.createSpyObj("FooService", {
someMethod: jasmine.createSpy(),
}),
},
],
});
effects = TestBed.inject(FooEffects);
fooServiceMock = TestBed.inject(FooService);
});
describe("effectWithDelay$", () => {
it("should dispatch doSomethingSuccess after 5 seconds if success", () => {
const someDataMock = { someData: Math.random() * 100 };
const initialAction = fromFooActions.doSomething(someDataMock);
const expectedAction = fromFooActions.doSomethingSuccess();
testScheduler.run((helpers) => {
// When the code inside this callback is being executed, any operator
// that uses timers/AsyncScheduler (like delay, debounceTime, etc) will
// **automatically** use the TestScheduler instead, so that we have
// "virtual time". You do not need to pass the TestScheduler to them,
// like in the past.
// https://rxjs-dev.firebaseapp.com/guide/testing/marble-testing
const { hot, cold, expectObservable } = helpers;
// Actions // -a-
// Service // -b|
// Results // 5s --c
// Actions
actions$ = hot("-a-", { a: initialAction });
// Service
fooServiceMock.someMethod.and.returnValue(cold("-b|", { b: null }));
// Results
expectObservable(effects.effectWithDelay$).toBe("5s --c", {
c: expectedAction,
});
});
// This needs to be outside of the run() callback
// since it's executed synchronously :O
expect(fooServiceMock.someMethod).toHaveBeenCalled();
expect(fooServiceMock.someMethod).toHaveBeenCalledTimes(1);
expect(fooServiceMock.someMethod).toHaveBeenCalledWith(someDataMock.someData);
});
});
});
Please notice that in the code I'm using expectObservable to test the effect using the "virtual time" from the TestScheduler instance.
you could use the done callback from jasmine
it('should dispatch action after 5 seconds', (done) => {
const resMock = 'resMock';
const entries: SubnetEntry[] = [{
type: 'type',
userText: 'userText',
ipAddress: '0.0.0.0'
}];
const action = new SubnetBrowserApiActions.LoadEntriesSucces({entries});
const completion = new SubnetBrowserApiActions.LoadEntriesSucces({entries});
actions$ = hot('-a', { a: action });
const response = cold('-a', {a: entries});
const expected = cold('- 5s b ', { b: completion });
subnetBrowserService.getSubnetEntries = () => (response);
effects.continuePollingEntries$.subscribe((res)=>{
expect(res).toEqual(resMock);
done()
})
});
The second notation doesn't work with jasmine-marbles, use dashes instead:
const expected = cold('------b ', { b: completion });
You will need to do 3 things
1- Inside your beforeEach, you need to override the internal scheduler of RxJs as follows:
import { async } from 'rxjs/internal/scheduler/async';
import { cold, hot, getTestScheduler } from 'jasmine-marbles';
beforeEach(() => {.....
const testScheduler = getTestScheduler();
async.schedule = (work, delay, state) => testScheduler.schedule(work, delay, state);
})
2- Replace delay, with delayWhen as follows:
delayWhen(_x => (true ? interval(50) : of(undefined)))
3- Use frames, I am not really sure how to use seconds for this, so I used frames. Each frame is 10ms. So for example my delay above is 50ms and my frame is -b, so that is the expected 10 ms + I needed another 50ms so this equals extra 5 frames which was ------b so as follows:
const expected = cold('------b ', { b: outcome });
I have two source observables.
I would like to merge the two source observables, but the merged observable sould complete as soon as one of the source observables completes.
Desired behavior:
Source 1: ---1--------3--4-----------------------------x
Source 2: -------2----------x
"merged" ---1---2----3--4--x
In case of an error on one of the sources, the error should propagate to the merged observable:
Source 1: ---1--------3--4-----------------------------x
Source 2: -------2----------e
"merged" ---1---2----3--4--ex
The "merge" operator only completes the merged stream when both sources have completed:
Source 1: ---1--------3--4-----------------------------x
Source 2: -------2----------x
"merged" ---1---2----3--4-----------------------------x
How can I achieve my desired behavior?
You need to work with the metadata, information about each observable. To do this, use the materialize() operator on each stream and the use dematerialize() on the merged stream to actually emit the data.
Observable.merge( observableA.materialize(),
observableB.materialize() )
.takeWhile( notification -> notification.hasValue() )
.dematerialize()
.subscribe( ... );
This will merge the two observables until either one of them completes or emits an error.
I sure hope someone else answers with more elegant method but this works.
I think you would have to use one of the take operators. You could complete all sources when one source completes like so:
const a = Rx.Observable.interval(1000).take(3).map(x => `a${x}`);
const b = Rx.Observable.interval(800).take(6).map(x => `b${x}`);
Rx.Observable.merge(a.takeUntil(b.last()), b.takeUntil(a.last()))
.subscribe(
x => { console.log('next', x); },
null,
() => { console.log('complete'); }
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/5.5.5/Rx.min.js"></script>
Or a less readable but more scaleable version:
function merge(...obs) {
return Rx.Observable.merge(...obs.map(x => x.takeUntil(Rx.Observable.race(obs.filter(y => y !== x).map(z => z.last())))));
}
const a = Rx.Observable.interval(1000).take(3).map(x => `a${x}`);
const b = Rx.Observable.interval(800).take(6).map(x => `b${x}`);
merge(a, b)
.subscribe(
x => { console.log('next', x); },
null,
() => { console.log('complete'); }
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/5.5.5/Rx.min.js"></script>
Here is an illustration with error propagation:
function merge(...obs) {
return Rx.Observable.merge(...obs.map(x => x.takeUntil(Rx.Observable.race(obs.filter(y => y !== x).map(z => z.last())))));
}
const a = Rx.Observable.interval(1000).take(3).map(x => `a${x}`);
const b = Rx.Observable.interval(800).take(6).map(x => `b${x}`);
const c = Rx.Observable.timer(2200).map(x => { throw 'oops!'; });
merge(a, b, c)
.subscribe(
x => { console.log('next', x); },
x => { console.log('error', x); },
() => { console.log('complete'); }
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/5.5.5/Rx.min.js"></script>
Using the takeUntil outside on the merge is tricky as you would loose the last emitted value.
When an an observable completes, it does not emit a value, but we can concat it with another 'signal' observable that emits a single value. We can then watch for the 'signal' observable's value with the takeWhile operator.
Of course you'd have to ensure that the 'signal' observable's emitted value is not a value that could be emitted by the observables that are being merged - an empty object will suffice if the takeWhile predicate compares by reference.
Here's an example:
const obs1$ = Rx.Observable.interval(1000)
.map(x => `obs1: ${x}`)
.take(5);
const obs2$ = Rx.Observable.interval(300)
.map(x => `obs2: ${x}`)
.take(9);
const signalFinishMessage = {};
const signalFinish$ = Rx.Observable.of(signalFinishMessage);
Rx.Observable.merge(obs1$.concat(signalFinish$), obs2$.concat(signalFinish$))
.takeWhile(x => x !== signalFinishMessage)
.subscribe(
x => console.log(x),
err => console.log('received error:', err),
() => console.log('complete')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/5.5.5/Rx.min.js"></script>
Errors will also get propagated:
const obs1$ = Rx.Observable.interval(1000)
.map(x => `obs1: ${x}`)
.take(5);
const obs2$ = Rx.Observable.interval(300)
.map(x => `obs2: ${x}`)
.take(9)
.concat(Rx.Observable.throw(`the world's about to end`));
const signalFinishMessage = {};
const signalFinish$ = Rx.Observable.of(signalFinishMessage);
Rx.Observable.merge(obs1$.concat(signalFinish$), obs2$.concat(signalFinish$))
.takeWhile(x => x !== signalFinishMessage)
.subscribe(
x => console.log(x),
err => console.log('received error:', err),
() => console.log('complete')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/5.5.5/Rx.min.js"></script>
I ended up rolling my own:
import { Observable } from 'rxjs';
export function whileAll<T>(...observables: Observable<T>[]): Observable<T> {
return new Observable<T>(function (observer) {
if (observables.length === 0)
observer.complete();
else {
const next = observer.next.bind(observer);
const error = observer.error.bind(observer);
const complete = observer.complete.bind(observer);
for (let i = 0; i < observables.length; i++)
observer.add(observables[i].subscribe(next, error, complete));
}
});
}