rxjs subscription being called more often than expected - rxjs

I have a BehaviorSubject stream of functions. I have an initialState object represented as an immutable Record. Those functions are scanned and used to manipulate the state. The code looks like this:
const initialState = Record({
todo: Record({
title: "",
}),
todos: List([Record({title: "first todo"})()])
})
const actionCreator = (update) => ({
addTodo(title) {
update.next((state) => {
console.log({title}); // for debugging reasons
const todo = Record({title})()
return state.set("todos", state.get("todos").push(todo))
})
},
typeNewTodoTitle(title) {
update.next((state) => state.set("todo", state.get("todo").set("title", title))
})
})
const update$ = new BehaviorSubject(state => state);
const actions = actionCreator(update$);
const state = update$.pipe(
scan(
(state, updater) => updater(state), initialState()
),
// share() without share weird things happen
)
I have a very simple test written for this
it("should only respond to and call actions once", () => {
const subscripition = chai.spy();
const addTodo = chai.spy.on(actions, 'addTodo');
const typeNewTodoTitle = chai.spy.on(actions, 'typeNewTodoTitle');
state
.pipe(
map(s => s.get("todo")),
distinctUntilChanged()
)
.subscribe(subscripition);
state
.pipe(
map(s => s.get("todos")),
distinctUntilChanged()
)
.subscribe(subscripition);
actions.addTodo('test');
expect(subscripition).to.have.been.called.twice // error
actions.typeNewTodoTitle('test');
expect(subscripition).to.have.been.called.exactly(3) // error
expect(addTodo).to.have.been.called.once
expect(typeNewTodoTitle).to.have.been.called.once
});
});
The first strange behavior is that subscription has been called 3 times and then 4 instead of 2 and then 3 times. The second strange behavior is that even though each action has only been called once, the console.log has been called twice. I can fix this problem by adding share() to the pipeline, but I can't figure out why that's required.

Related

Redux Observable - How to send off an action to start a separate epic, then wait for that epics response (or timeout)

So I basically have a websocket connection, this allows me to send generic messages via WEBSOCKET_MESSAGE_SEND and receive them via WEBSOCKET_MESSAGE_RECEIVED actions.
However there are cases where I want to make a request in a similar manner to a Ajax REST call. Eg to request a list of documents for a user I probably want to have an epic:
Receive an action eg ({ type: GET_DOCUMENTS })
Generate a random key to track the current request, we will call it 'request_id'
Send a ({ type: WEBSOCKET_MESSAGE_SEND, request_id }) action.
Wait for either of
an action ({ type: WEBSOCKET_MESSAGE_RECEIVED, request_id, message }) **Must be with a matching 'request_id' otherwise it should be ignored.
-> Emit an action eg ({ type: GET_DOCUMENTS_SUCCESS, documents: message })
a timeout eg 10 seconds
-> Emit an action eg ({ type: GET_DOCUMENTS_TIMEOUT })
I have been struggling to put this into code, I think the most awkward part of the whole epic is that I want to emit an action in the middle of my epic and wait. This doesn't feel quite right to me... ani-pattern? But I am not really sure how I should be doing this.
That's right. There is no good way to emit an action in the middle of an epic. How about splitting the epic into two?
const getDocumentsEpic = action$ =>
action$.pipe(
ofType("GET_DOCUMENTS"),
map(() => {
const requestId = generateRequestId();
return {
type: "WEBSOCKET_MESSAGE_SEND",
requestId
};
})
);
const websocketMessageEpic = action$ =>
action$.pipe(
ofType("WEBSOCKET_MESSAGE_SEND"),
switchMap(requestId => {
return action$.pipe(
ofType("WEBSOCKET_MESSAGE_RECEIVED"),
filter(action => action.requestId === requestId),
timeout(10000),
map(({ message }) => ({
type: "GET_DOCUMENTS_SUCCESS",
documents: message
})),
catchError(() => of({ type: "GET_DOCUMENTS_TIMEOUT" }))
);
})
);
Updated answer (2020-04-17):
I was unhappy with my original answer so decided to give it another shot.
NotificationOperators.js
import { of } from 'rxjs';
import { map, switchMap, filter, timeout, catchError, first, mergeMap } from 'rxjs/operators';
import { notificationActionTypes } from '../actions';
const NOTIFICATION_TIMEOUT = 60 * 1000;
const generateRequestId = () => Math.random().toString(16).slice(2);
const toNotificationRequest = notificationRequest => input$ =>
input$.pipe(mergeMap(async action => ({
type: notificationActionTypes.WEBSOCKET_MESSAGE_SEND,
message: {
request_id: generateRequestId(),
...(
typeof notificationRequest === "function" ?
await Promise.resolve(notificationRequest(action)) :
({ eventType: notificationRequest })
)
}
})));
const mapNotificationRequestResponses = (notificationRequest, mapper) => $input =>
$input.pipe(
filter(action =>
action.type === notificationActionTypes.WEBSOCKET_MESSAGE_SEND &&
action.message.eventType === notificationRequest),
concatMap(sendAction =>
$input.pipe(
filter(receiveAction => {
return (
receiveAction.type === notificationActionTypes.WEBSOCKET_MESSAGE_RECEIVED &&
receiveAction.message.request_id === sendAction.message.request_id
)
}),
first(),
timeout(NOTIFICATION_TIMEOUT),
map(({ message }) => mapper(message.success ? false : message.error, message.result, sendAction.message)),
catchError(errorMessage => of(mapper(errorMessage && errorMessage.message, null, sendAction.message))))));
export { toNotificationRequest, mapNotificationRequestResponses };
Usage:
export const getDocumentsReqEpic = action$ => action$.pipe(
ofType(documentActionTypes.REFRESH_DOCUMENTS_REQUEST),
toNotificationRequest(EventTypes.get_user_documents_req)
);
export const getDocumentsRecEpic = action$ => action$.pipe(
mapNotificationRequestResponses(
EventTypes.get_user_documents_req,
(error, result) => error ? refreshDocumentsError(error) : refreshDocumentsSuccess(result))
);
Original answer:
As I felt I would likely need to repeat this process many more times, this seemed like a reasonable amount of duplicated boilterplate that I should create a method to generate epics based on requirements. For this reason I have expanded upon #sneas awesome answer and have posted below incase it helps others.
Note this implementation assumes the websocket implementation from the other answer. It also assumes that the server websocket implementation will accept a 'request_id' and respond with the same 'request_id' so that request and response messages can be linked. Probably also worth noting that the 'epicLinkId' is client-side only, and simply enables the 2 epics being created to be linked to each other, without this you would only be able to call createNotifyReqResEpics() once.
createNotifyReqResEpics.js (helper based on code above)
import { ofType } from 'redux-observable';
import { of } from 'rxjs';
import { map, switchMap, filter, timeout, catchError, first } from 'rxjs/operators';
import { notificationActionTypes } from '../actions';
const generateRequestId = () => Math.random().toString(16).slice(2);
export default ({
requestFilter,
requestMessageMapper,
responseMessageMapper
}) => {
if (typeof requestFilter !== "function")
throw new Error("Invalid function passed into createNotifyReqResEpics 'requestFilter' argument.");
if (typeof requestMessageMapper !== "function")
throw new Error("Invalid function passed into createNotifyReqResEpics 'requestMessageMapper' argument.");
if (typeof responseMessageMapper !== "function")
throw new Error("Invalid function passed into createNotifyReqResEpics 'responseMessageMapper' argument.");
const epicLinkId = generateRequestId();
const websocketSendEpic = action$ =>
action$.pipe(
filter(requestFilter),
map(action => ({
epic_link_id: epicLinkId,
type: notificationActionTypes.WEBSOCKET_MESSAGE_SEND,
message: {
request_id: generateRequestId(),
...requestMessageMapper(action)
}
}))
);
const websocketReceiveEpic = action$ =>
action$.pipe(
ofType(notificationActionTypes.WEBSOCKET_MESSAGE_SEND),
filter(action => action.epic_link_id === epicLinkId),
switchMap(sendAction =>
action$.pipe(
ofType(notificationActionTypes.WEBSOCKET_MESSAGE_RECEIVED),
filter(receiveAction => receiveAction.request_id === sendAction.request_id),
first(),
timeout(10000),
map(receiveAction => responseMessageMapper(false, receiveAction.message)),
catchError(errorMessage => of(responseMessageMapper(errorMessage && errorMessage.message, null))))));
return [websocketSendEpic, websocketReceiveEpic];
};
documents.js (epics)
import EventTypes from '../shared-dependencies/EventTypes';
import { documentActionTypes, refreshDocumentsError, refreshDocumentsSuccess } from '../actions';
import { createNotifyReqResEpics } from '../utils';
const [getDocumentsReqEpic, getDocumentsRespEpic] = createNotifyReqResEpics({
requestFilter: action => action.type === documentActionTypes.REFRESH_DOCUMENTS_REQUEST,
requestMessageMapper: action => ({ eventType: EventTypes.get_user_documents_req }),
responseMessageMapper: (error, action) => error ? refreshDocumentsError(error) : refreshDocumentsSuccess(action.result)
});
export { getDocumentsReqEpic, getDocumentsRespEpic };
Where the 2 exported epics from documents.js make thie way into combineEpics.

Multiple subscriptions nested into one subscription

I find myself puzzled trying to set a very simple rxjs flow of subscriptions. Having multiple non-related subscriptions nested into another.
I'm in an angular application and I need a subject to be filled with next before doing other subscriptions.
Here would be the nested version of what I want to achieve.
subject0.subscribe(a => {
this.a = a;
subject1.subscribe(x => {
// Do some stuff that require this.a to exists
});
subject2.subscribe(y => {
// Do some stuff that require this.a to exists
});
});
I know that nested subscriptions are not good practice, I tried using flatMap or concatMap but didn't really get how to realize this.
It's always a good idea to separate the data streams per Observable so you can easily combine them later on.
const first$ = this.http.get('one').pipe(
shareReplay(1)
)
The shareReplay is used to make the Observable hot so it won't call http.get('one') per each subscription.
const second$ = this.first$.pipe(
flatMap(firstCallResult => this.http.post('second', firstCallResult))
);
const third$ = this.first$.pipe(
flatMap(firstCallResult => this.http.post('third', firstCallResult))
);
After this you can perform subscriptions to the Observables you need:
second$.subscribe(()=>{}) // in this case two requests will be sent - the first one (if there were no subscribes before) and the second one
third$.subscribe(() => {}) // only one request is sent - the first$ already has the response cached
If you do not want to store the first$'s value anywhere, simply transform this to:
this.http.get('one').pipe(
flatMap(firstCallResult => combineLatest([
this.http.post('two', firstCallResult),
this.http.post('three', firstCallResult)
])
).subscribe(([secondCallResult, thirdCallResult]) => {})
Also you can use BehaviorSubject that stores the value in it:
const behaviorSubject = new BehaviorSubject<string>(null); // using BehaviorSubject does not require you to subscribe to it (because it's a hot Observable)
const first$ = behaviorSubject.pipe(
filter(Boolean), // to avoid emitting null at the beginning
flatMap(subjectValue => this.http.get('one?' + subjectValue))
)
const second$ = first$.pipe(
flatMap(firstRes => this.http.post('two', firstRes))
)
const third$ = first$.pipe(
flatMap(()=>{...})
)
behaviorSubject.next('1') // second$ and third$ will emit new values
behaviorSubject.next('2') // second$ and third$ will emit the updated values again
You can do that using the concat operator.
const first = of('first').pipe(tap((value) => { /* doSomething */ }));
const second = of('second').pipe(tap((value) => { /* doSomething */ }));
const third = of('third').pipe(tap((value) => { /* doSomething */ }));
concat(first, second, third).subscribe();
This way, everything is chained and executed in the same order as defined.
EDIT
const first = of('first').pipe(tap(value => {
// doSomething
combineLatest(second, third).subscribe();
}));
const second = of('second').pipe(tap(value => { /* doSomething */ }));
const third = of('third').pipe(tap(value => { /* doSomething */ }));
first.subscribe();
This way, second and third are running asynchronously as soon as first emits.
You could do something like this:
subject$: Subject<any> = new Subject();
this.subject$.pipe(
switchMap(() => subject0),
tap(a => {
this.a = a;
}),
switchMap(() => subject1),
tap(x => {
// Do some stuff that require this.a to exists
}),
switchMap(() => subject2),
tap(y => {
// Do some stuff that require this.a to exists
})
);
if you want to trigger this, simply call this.subject$.next();
EDIT:
Here is an possible approach with forkJoin, that shout call the subjects parallel.
subject$: Subject<any> = new Subject();
this.subject$.pipe(
switchMap(() => subject0),
tap(a => {
this.a = a;
}),
switchMap(
() => forkJoin(
subject1,
subject2
)),
tap([x,y] => {
// Do some stuff that require this.a to exists
})
);

How doi I compose subscriptions with RxJS?

Is this a good way to compose different subscriptions/streams?
I'm creating 2 programs (terminal streams) that start each other up and run until the clean up function that is returned is called.
const program = () => {
let activeSubscription;
const program1 = () => fromEvent(document, 'mousemove').pipe(take(10)).subscribe(
console.log,
console.error,
() => { activeSubscription = program2() }
)
const program2 = () => fromEvent(document, 'click').pipe(take(10)).subscribe(
console.log,
console.error,
() => { activeSubscription = program1()
)
// Return clean up function
return () => activeSubscription && activeSubscription.unsubscribe()
It's better to avoid dealing with multiple subscriptions and combine observables in a single stream:
concat(
fromEvent(document, 'mousemove').pipe(take(10), tap(() => console.log('move'))),
fromEvent(document, 'click').pipe(take(10), tap(() => console.log('click')))
).pipe(repeat()).subscribe();
This way you don't have to subscribe right away and your program can return observable to be piped/subscribed later somewhere else. It gives more flexibility.
In the Angular world we have used the standard of using a takeUntil on each subscription with a finalise subject. That way in your clean up function you emit down a single subject and all other subscriptions are cleaned up.
const program = () => {
let finalise = new Subject();
const program1 = () => fromEvent(document, 'mousemove').pipe(take(10), takeUntil(finalise)).subscribe(
console.log,
console.error
)
const program2 = () => fromEvent(document, 'click').pipe(take(10), takeUntil(finalise)).subscribe(
console.log,
console.error
)
// Return clean up function
return () => {
finalise.next();
finalise.complete();
}
}

Testing NGRX effect with delay

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 });

rxjs switchMap need to return subscribed observable

Here here the requirement:
When click start button, emit event x times every 100ms, each emit correspond an UI update. When x times emit complete, it will trigger a final UI update, look simple right?
Here is my code:
const start$ = fromEvent(document.getElementById('start'), 'click')
const intervel$ = interval(100)
.pipe(
take(x),
share()
)
var startLight$ = start$
.pipe(
switchMap(() => {
intervel$
.pipe(last())
.subscribe(() => {
// Update UI
})
return intervel$
}),
share()
)
startLight$
.subscribe(function (e) {
//Update UI
})
Obviously, subscribe inside switchMap is anti-pattern, so I tried to refactor my code:
const startInterval$ = start$
.pipe(
switchMapTo(intervel$),
)
startInterval$.pipe(last())
.subscribe(() => {
//NEVER Receive value
})
const startLight$ = startInterval$.pipe(share())
The problem is that intervel$ stream is generated inside switchMap and can not be accessed outside, you can only access the stream who generate interval$, i.e. start$ which never complete!
Is there is smarter way to handle such kind of problem or it was an inherent limitation of rxjs?
You were very close. Use last() inside intervel$ to only emit the final one to the subscribe below. Working StackBlitz. Here are details from the StackBlitz:
const start$ = fromEvent(document.getElementById('start'), 'click');
const intervel$ = interval(100)
.pipe(
tap(() => console.log('update UI')), // Update UI here
take(x),
last()
);
const startInterval$ = start$
.pipe( switchMapTo(intervel$));
startInterval$
.subscribe(() => {
console.log('will run once');
});
Update
If you do not wish to use tap(), then you can simply cause start$ to finish by taking only the first emission and then completing with either take(1) or first(). Here is a new StackBlitz showing this.
const start$ = fromEvent(document.getElementById('start'), 'click')
.pipe(
first()
);
const intervel$ = interval(100)
.pipe(
take(x)
);
const startInterval$ = start$
.pipe(
switchMapTo(intervel$)
);
startInterval$
.subscribe(
() => console.log('Update UI'),
err => console.log('Error ', err),
() => console.log('Run once at the end')
);
The downside to this approach (or any approach that completes the Observable) is that once completed it won't be reused. So for example, clicking multiple times on the button in the new StackBlitz won't work. Which approach to use (the first one that can be clicked over and over or the one that completes) depends on the results you need.
Yet Another Option
Create two intervel$ observables, one for the intermediate UI updates and one for the final one. Merge them together and only do the UI updating in the subscribe. StackBlitz for this option
code:
const start$ = fromEvent(document.getElementById('start'), 'click')
const intervel1$ = interval(100)
.pipe(
take(x)
);
const intervel2$ = interval(100)
.pipe(
take(x+1),
last(),
mapTo('Final')
);
const startInterval$ = start$
.pipe(
switchMapTo(merge(intervel1$, intervel2$))
);
startInterval$
.subscribe(
val => console.log('Update UI: ', val)
);
A more idiomatic way, same logic as previous one (By Guichi)
import { switchMapTo, tap, take, last, share, mapTo } from 'rxjs/operators';
import { fromEvent, interval, merge } from 'rxjs';
const x = 5;
const start$ = fromEvent(document.getElementById('start'), 'click');
const intervel$ = interval(100);
const intervel1$ = intervel$
.pipe(
take(x)
);
const intervel2$ = intervel1$
.pipe(
last(),
mapTo('Final')
);
const startInterval$ = start$
.pipe(
switchMapTo(merge(intervel1$, intervel2$))
);
startInterval$
.subscribe(
val => console.log('Update UI: ', val)
);
Reflection
The key problem of the original question is to 'use the same observable in different ways', i.e. during the progress and the final. So merge is an pretty decent logic pattern to target this kind of problem
Put your update logic inside the switchMap and tap() , tap will run multiple time and only last emission will be taken by subscribe()
const startInterval$ = start$
.pipe(
switchMap(()=>intervel$.pipe(tap(()=>//update UI),last()),
)
startInterval$
.subscribe(() => {
// will run one time
})

Resources