I am trying to simulate the "brush" feature like the one in any image editor.
I have the following streams:
$pointerDown: pointer pressed down
$pointerUp: pointer pressed up
$position: position of the brush
$escape: Escape key pressed
What I want to do
When the user is dragging the mouse, do temporary calculations. If the mouse is up, then commit those changes. If the escape key is pressed then do not commit those changes.
What I am currently handling is the first case:
$pointerDown.pipe(
r.switchMap(() =>
$position.pipe(
r.throttleTime(150),
r.map(getNodesUnderBrush),
r.tap(prepareChanges),
r.takeUntil($pointerUp),
r.finalize(commitBrushStroke))
)).subscribe()
How can I end the stream in two different ways? What is the idiomatic rxjs for this?
Thanks
Regarding your question I can see you need to have some kind of state over time. Here your state is the pointerdown/move/dragging observable, that needs to be accumulated or cleared and finally emitted. When I see such a state scenario I always like to use the scan operator:
Pre
For the sake of simple example i did not use your predefined observables. If you have issues adapting your specific pointer usecase to this very similar one, I can try to update it so it is closer to your question
1. What could represent my state
Here I am using an enum [status] to later on react on the event that happened before and an accumulation [acc] for the points over time
interface State {
acc: number[],
status: Status
}
enum Status {
init,
move,
finish,
escape
}
const DEFAULT_STATE: State = {
acc: [],
status: Status.init
}
2. Write functions that mutate the state
Your requirement can be split into: accumulate [pointerdown$ + position$], finish [pointerup$], escape [escape$]
const accumulate = (index: number) => (state: State): State =>
({status: Status.move, acc: [...state.acc, index]});
const finish = () => (state: State): State =>
({status: Status.finish, acc: state.acc})
const escape = () => (state: State): State =>
({status: Status.escape, acc: []})
3. Map your functions to your observables
merge(
move$.pipe(map(accumulate)),
finish$.pipe(map(finish)),
escape$.pipe(map(escape))
)
4. Use the functions in the scan where your state over time is placed
scan((acc: State, fn: (state: State) => State) => fn(acc), DEFAULT_STATE)
5. Process your mutated state
Here we only want to process if we have a finish, so we filter for it
filter(state => state.status === Status.finish),
Inner state sample
move$.next('1') = State: {status: move, acc: ['1']}
escape$.next() = State: {status: escape, acc: []}
move$.next('2') = State: {status: move, acc: ['2']}
finish$.next() = State: {status: finish, acc: ['2']}
Running stackblitz
FYI: It is pretty hard to get this mindset of solving state problems with rxjs. But once you understand the flow behind the idea, you can use it in nearly every statefull scenario. You will avoid sideeffects, stick to rxjs workflow and you can easily optimize/debugg your code.
Very interesting problem!
Here's my approach:
const down$ = fromEvent(div, 'mousedown');
const up$ = fromEvent(div, 'mouseup');
const calc$ = of('calculations');
const esc$ = fromEvent(document, 'keyup')
.pipe(
filter((e: KeyboardEvent) => e.code === 'Escape')
);
down$ // The source of the stream - mousedown events
.pipe(
switchMapTo(calc$.pipe( // Do some calculations
switchMapTo(
merge( // `merge()` - want to stop making the calculations when either `up$` or `esc$` emit
up$.pipe(mapTo({ shouldCommit: true })),
esc$.pipe(mapTo({ shouldCommit: false })),
).pipe(first()) // `first()` - either of these 2 can stop the calculations;
)
))
)
.subscribe(console.log)
StackBlitz.
I think there's pretty simple way to do that with toArray() and takeUntil(). I'm assuming that when you said you want "commit" changes you want to collect all changes and process them all at once. Otherwise, the same approach would work with buffer() as well.
$pointerDown.pipe(
switchMap(() => $position.pipe(
throttleTime(150),
map(getNodesUnderBrush),
tap(prepareChanges), // ?
takeUntil($pointerUp),
toArray(),
takeUntil($escape),
)
).subscribe(changes => {
...
commitBrushStroke(changes);
})
So the entire trick is whether you complete the inner chain before or after toArray. When you complete it before toArray() then toArray() will emits a single array of all changes it has collected so far. If you complete it after toArray() then the chain is disposed and toArray() will discard everything and just unsubscribe.
If I understand right, when the user presses the escape key than the entire stream should be unsubscribed so that, when the users starts again the dragging with the pointer mouse down, the stream starts again emitting.
If this is the case, you may want to try switchMap with $escape and merge, in other words something like this
const drag$ = $pointerDown.pipe(
r.switchMap(() =>
$position.pipe(
r.throttleTime(150),
r.map(getNodesUnderBrush),
r.tap(prepareChanges),
r.takeUntil($pointerUp),
r.finalize(commitBrushStroke))
))
const brush$ = $escape.pipe(
startWith({}), // emit any value at start just to allow the stream to start
switchMap(() => drag$)
);
const stopBrush$ = $escape.pipe(tap(() => // do stuff to cancel));
merge(brush$, stopBrush$).subscribe();
The whole idea is that, any time $escape emits, the previous subscription to drag$ is unsubscribed and a new one starts. At the same time any logic to cancel what needs to be cancelled can be performed.
I can not test this thing, so I hope I have not forgot something.
Here goes an other possible solution very much like your original:
const start$ = fromEvent(document, "mousedown").pipe(
tap((event: MouseEvent) => this.start = `x: ${event.clientX}, y: ${event.clientY}`)
);
const drag$ = fromEvent(document, "mousemove").pipe(
tap((event: MouseEvent) => (this.move = `x: ${event.clientX}, y: ${event.clientY}`) )
);
const stop$ = fromEvent(document, "mouseup").pipe(
tap((event: MouseEvent) => (this.stop = `x: ${event.clientX}, y: ${event.clientY}`))
);
const cancel$ = fromEvent(document, "keydown").pipe(
filter((e: KeyboardEvent) => e.code === "Escape"),
tap(() => this.stop = 'CANCELLED')
);
const source$ = start$
.pipe(
mergeMap(() =>
drag$.pipe(
takeUntil(merge(stop$, cancel$))
)
)
)
.subscribe();
The stream can end in two ways merging both takeUntil conditions:
takeUntil(merge(stop$, cancel$))
Here is the Stackblitz: https://stackblitz.com/edit/angular-gw7gyr
Related
I found this article that explains how I can use RxJs to create an observable for auto-complete:
https://blog.strongbrew.io/building-a-safe-autocomplete-operator-with-rxjs
const autocomplete = (time, selector) => (source$) =>
source$.pipe(
debounceTime(time),
switchMap((...args: any[]) =>
selector(...args)
.pipe(
takeUntil(
source$
.pipe(
skip(1)
)
)
)
)
)
term$ = new BehaviorSubject<string>('');
results$ = this.term$.pipe(
autocomplete(1000, (term => this.fetch(term)))
)
I want to improve this auto-complete observable by first returning data from local storage and display it to the user and then continue to the server to fetch data. The data that will be returned from the server will not replace the one the result from the local storage but will be added to it.
If I understand it correctly on each time the user types, there observable should emit twice.
How can I build it in the most efficient way?
Kind Regards,
Tal Humy
I think you can take advantage of startWith.
const term$ = new BehaviorSubject('');
const localStorageResults = localStorage.getItem(''); // Map it into the same shape as results$ but the observable unwrapped
const results$ = term$
.pipe(
startWith(localStorageResults),
debounceTime(1000),
switchMap(term =>
getAutocompleteSuggestions(term)
.pipe(
takeUntil(
//skip 1 value
term$.pipe(skip(1))
)
)
)
)
)
You may have to tinker with that, I am not sure if it will play nice with the debounceTime but it's an idea.
So after dealing with this for a few hours, I figured out that the solution was very straightforward:
autocomplete(1000, (term => new Observable(s => {
const storageValue = this.fetchFromStorage(term);
s.next(storageValue);
this.fetchFromServer(term)
.subscribe(r => s.next(r));
})))
I have a redux state using redux-observable's epics.
I need to solve showing a message after a user deletes an object or more objects.
There are two ways how to delete an object:
by action deleteObject(id: string) which call deleteObjectFulfilled action
by action deleteObjects(ids: Array<string>) which call N * deleteObject(id: string) actions
I want to show only one message with a count of deleted messages after every success "deleting action".
My final solution of this epic is:
export const showDeleteInformationEpic = action$ =>
combineLatest(
action$.pipe(ofType(DELETE_OBJECT_FULFILLED)),
action$.pipe(
ofType(DELETE_OBJECTS),
switchMap(({ meta: { ids } }) =>
action$.pipe(
ofType(DELETE_OBJECT_FULFILLED),
skip(ids.length - 1),
map(() => ids.length),
startWith('BATCH_IN_PROGRESS'),
take(2),
),
),
startWith(1),
),
).pipe(
startWith([null, null]),
pairwise(),
map(([[, previousCount], [, currentCount]]) =>
(previousCount === 'BATCH_IN_PROGRESS')
? currentCount
: isNumber(currentCount) ? 1 : currentCount),
filter(isNumber),
map((count) => throwInfo('objectDeleted', { count })),
);
Can you see any better solution of this?
There is more simple solution if I use only deleteObjects(Array<string>) for both cases..
Instead of firing multiple actions, you can create and dispatch a single action DELETE_MULTIPLE and pass all the id(s) in the payload.
This way your effects will be a lot cleaner since you only have to subscribe to DELETE_MANY action and additionally, it will prevent multiple store dispatches.
Let's say we have this global const:
const isSignedIn = fromPromise(fetch('/api/is-signed-in'))
.pipe(throttleTime(1000), shareReply(1));
After page load, several components will subscribe to this at the same time:
isSignedIn.subscribe(() => console.log('do 1st'));
isSignedIn.subscribe(() => console.log('do 2nd'));
isSignedIn.subscribe(() => console.log('do 3rd'));
The above will only call the API once, however i need it to call the API again (ie after 1 second) if another component subscribes to it.
isSignedIn.subscribe(() => console.log('button press'));
How do i that using RxJS?
I think this is what you want:
A pipeable operator (declare globally somewhere and import it)
export const refreshAfter = (duration: number) => (source: Observable<any>) =>
source.pipe(
repeatWhen(obs => obs.pipe(delay(duration))),
publishReplay(1),
refCount());
Then use it like this:
data$ = fetch('/api/is-signed-in').pipe(refreshAfter(5000)); // refresh after 5000 ms
Note: You actually asked for this:
i need it to call the API again (ie after 1 second) if another component subscribes to
it.
Not quite sure this is what you really meant. I think what you really meant was - you want the data to be refreshed for all components currently subscribed after an expiry time. Anyway my answer sends the new value to all listeners. If you really want what you originally said you'd need to add some kind of alternative repeat trigger.
But if this is for a global constant - the above is what I'm using for the same scenario.
Note: I haven't actually tested the handling of an error condition when the item is repested, but I think the error will propagate to all listeners.
If we reimplement ShareReplay so it:
- will never unsubscribe from source even if it have no more subscribers (remove refCount, potential memory leak).
- accept rerunAfter argument, time passed from last subscribe to source.
import {Subject, of, Observable, ReplaySubject, Subscriber} from 'rxjs';
import {pluck, shareReplay, tap, delay} from 'rxjs/operators';
function shareForeverReplayRerun<T>(bufferSize: number, rerunAfter: number) {
let subject;
let subscription;
let hasError = false;
let isComplete = false;
let lastSubTime = 0;
return source => Observable.create((observer: Subscriber<T>) => {
if (!subject || hasError || (Date.now() - lastSubTime) >= rerunAfter) {
lastSubTime = Date.now();
hasError = false;
subject = new ReplaySubject<T>(bufferSize);
subscription = source.subscribe({
next(value) { subject.next(value); },
error(err) {
hasError = true;
subject.error(err);
},
complete() {
isComplete = true;
subject.complete();
},
});
}
const innerSub = subject.subscribe(observer);
// never unsubscribe from source
return () => {
innerSub.unsubscribe();
};
})
}
const source = of('Initial').pipe(
tap(()=>console.log('COMPUTE')),
delay(200),
shareReplayRerun(1, 1000),
);
source.subscribe(console.log.bind(null, 'syncI:'));
source.subscribe(console.log.bind(null, 'syncII:'));
setTimeout(()=>source.subscribe(console.log.bind(null, 'after500:')), 500);
setTimeout(()=>source.subscribe(console.log.bind(null, 'after900:')), 900);
setTimeout(()=>source.subscribe(console.log.bind(null, 'after1500:')), 1500);
as output we have:
COMPUTE
syncI: Initial
syncII: Initial
after500: Initial
after900: Initial
COMPUTE
after1500:Initial
EDITED: The answer is wrong. BufferSize is how long the last N events are replayed. After this the stream is completed.
signature: shareReplay(
bufferSize?: number,
windowTime?: number,
scheduler?: IIScheduler
):Observable
#param {Number} [bufferSize=Number.POSITIVE_INFINITY] Maximum element count of the replay buffer.
#param {Number} [windowTime=Number.MAX_VALUE] Maximum time length of the replay buffer in milliseconds.
Try to add 1000 as second argument to shareReply:
const isSignedIn = fromPromise(fetch('/api/is-signed-in'))
.pipe(throttleTime(1000), shareReplay(1, 1000));
shareReplay.ts - be care of refCount-- on unsubcribe as it can trigger additional requests.
I want to buffer events sent to my server. The trigger to flush the buffer is either the buffer size has been reached, buffer period has been reached or the window has been unloaded.
I buffer events sent to my server by creating a Subject and using buffer with a closing notifier. I use race for the closing notifier and race the buffer period with with window.beforeunload event.
this.event$ = new Subject();
this.bufferedEvent$ = this.event$
.buffer(
Observable.race(
Observable.interval(bufferPeriodMs),
Observable.fromEvent(window, 'beforeunload')
)
)
.filter(events => events.length > 0)
.switchMap(events =>
ajax.post(
this.baseUrl + RESOURCE_URL,
{
entries: events,
},
{
'Content-Type': 'application/json',
}
)
);
The question is, how do I now also limit the size of the buffer. ie, I never want the buffer to be flushed when it has 10 items.
This is the working solution I have. Extra console.log()'s are added to show the sequence of events.
The only thing that's a bit bothersome is the .skip(1) in fullBufferTrigger, but it's needed as it will trigger when it's buffer is full (natch), but the buffer in bufferedEvent$ does not seem to have the latest event before it's triggered.
Luckily, with the timeoutTrigger in place, the last event gets emitted. Without timeout, fullBufferTrigger by itself will not emit the final event.
Also, changed buffer to bufferWhen, as the former did not seem to trigger with two triggers, although you'd expect it to from the documentation.
footnote with buffer(race()) the race only completes once, so whichever trigger got there first will thereafter be used and the other triggers dis-regarded. In contrast, bufferWhen(x => race()) evaluates every time an event occurs.
const bufferPeriodMs = 1000
const event$ = new Subject()
event$.subscribe(event => console.log('event$ emit', event))
// Define triggers here for testing individually
const beforeunloadTrigger = Observable.fromEvent(window, 'beforeunload')
const fullBufferTrigger = event$.skip(1).bufferCount(2)
const timeoutTrigger = Observable.interval(bufferPeriodMs).take(10)
const bufferedEvent$ = event$
.bufferWhen( x =>
Observable.race(
fullBufferTrigger,
timeoutTrigger
)
)
.filter(events => events.length > 0)
// output
fullBufferTrigger.subscribe(x => console.log('fullBufferTrigger', x))
timeoutTrigger.subscribe(x => console.log('timeoutTrigger', x))
bufferedEvent$.subscribe(events => {
console.log('subscription', events)
})
// Test sequence
const delayBy = n => (bufferPeriodMs * n) + 500
event$.next('event1')
event$.next('event2')
event$.next('event3')
setTimeout( () => {
event$.next('event4')
}, delayBy(1))
setTimeout( () => {
event$.next('event5')
}, delayBy(2))
setTimeout( () => {
event$.next('event6')
event$.next('event7')
}, delayBy(3))
Working example: CodePen
Edit: Alternative way to trigger the buffer
Since the combination of bufferWhen and race might be a bit inefficient (the race is restarted each event emission), an alternative is to merge the triggers into one stream and use a simple buffer
const bufferTrigger$ = timeoutTrigger
.merge(fullBufferTrigger)
.merge(beforeunloadTrigger)
const bufferedEvent$ = event$
.buffer(bufferTrigger$)
.filter(events => events.length > 0)
One thing that bothers me about the solution using independent triggers is that fullBufferTrigger doesn't know when timeoutTrigger has emitted one of it's buffered values, so given the right event sequence, fullBuffer will trigger early when following timeout.
Ideally, would want fullBufferTrigger to reset when timeoutTrigger fires, but that proves tricky to do.
Using bufferTime()
In RxJS v4 there was an operator bufferWithTimeOrCount(timeSpan, count, [scheduler]), which in RxJS v5 was rolled up into an additional signature of bufferTime() (arguably a mistake from the perspective of clarity).
bufferTime<T>(
bufferTimeSpan: number,
bufferCreationInterval: number,
maxBufferSize: number,
scheduler?: IScheduler
): OperatorFunction<T, T[]>;
The only remaining question is how to incorporate the window.beforeunload trigger. Looking at the source code for bufferTime, it should flush it's buffer when receiving onComplete.
So, we can handle window.beforeunload by sending an onComplete to the buffered event stream.
The spec for bufferTime does not have an explicit test for onComplete, but I think I've managed to put one together.
Notes:
the timeout is set large to take it out of the picture for the test.
the source event stream is not affected, to illustrate event8 is added but never emits because the window is destroyed before it occurs.
to see the output stream without the beforeunloadTrigger, comment out the line that emits onComplete. Event7 is in the buffer, but will not emit.
Test:
const bufferPeriodMs = 7000 // Set high for this test
const bufferSize = 2
const event$ = new Rx.Subject()
/*
Create bufferedEvent$
*/
const bufferedEvent$ = event$
.bufferTime(bufferPeriodMs, null, bufferSize)
.filter(events => events.length > 0)
const subscription = bufferedEvent$.subscribe(console.log)
/*
Simulate window destroy
*/
const destroy = setTimeout( () => {
subscription.unsubscribe()
}, 4500)
/*
Simulate Observable.fromEvent(window, 'beforeunload')
*/
const beforeunloadTrigger = new Rx.Subject()
// Comment out the following line, observe that event7 does not emit
beforeunloadTrigger.subscribe(x=> event$.complete())
setTimeout( () => {
beforeunloadTrigger.next('unload')
}, 4400)
/*
Test sequence
Event stream: '(123)---(45)---6---7-----8--|'
Destroy window: '-----------------------x'
window.beforeunload: '---------------------y'
Buffered output: '(12)---(34)---(56)---7'
*/
event$.next('event1')
event$.next('event2')
event$.next('event3')
setTimeout( () => { event$.next('event4'); event$.next('event5') }, 1000)
setTimeout( () => { event$.next('event6') }, 3000)
setTimeout( () => { event$.next('event7') }, 4000)
setTimeout( () => { event$.next('event8') }, 5000)
Working example: CodePen
This question is an extension of my previous question that you can find here:
How to use RxJS to display a "user is typing" indicator?
After having successfully been able to track whether or not the user was typing, I needed to be able to use that particular state as a trigger for a clock.
The logic is simple, essentially I want a clock to be run when the user is typing. But when the user stops typing, I need the clock to pause. When the user starts typing again, the clock should continue to accumulate.
I have already been able to get it to work, but it looks like a mess and I need help refactoring it so it isn't a ball of spaghetti. Here is what the code looks like:
/*** Render Functions ***/
const showTyping = () =>
$('.typing').text('User is typing...');
const showIdle = () =>
$('.typing').text('');
const updateTimer = (x) =>
$('.timer').text(x);
/*** Program Logic ***/
const typing$ = Rx.Observable
.fromEvent($('#input'), 'input')
.switchMapTo(Rx.Observable
.never()
.startWith('TYPING')
.merge(Rx.Observable.timer(1000).mapTo('IDLE')))
.do(e => e === 'TYPING' ? showTyping() : showIdle());
const timer$ = Rx.Observable
.interval(1000)
.withLatestFrom(typing$)
.map(x => x[1] === 'TYPING' ? 1 : 0)
.scan((a, b) => a + b)
.do(console.log)
.subscribe(updateTimer)
And here is the link to the live JSBin: http://jsbin.com/lazeyov/edit?js,console,output
Perhaps I will walk you through the logic of the code:
I first build a stream to capture each typing event.
For each of these events, I will use switchMap to: (a) fire off the original "TYPING" event so we don't lose it, and (b) fire off an "IDLE" event, 1 second later. You can see that I create these as separate streams and then merge them together. This way, I get a stream that will indicate the "typing state" of the input box.
I create a second stream that sends an event every second. Using withLatestFrom, I combine this stream with the previous "typing state" stream. Now that they are combined, I can check whether or not the typing state is "IDLE" or "TYPING". If they are typing, I give the event a value of 1, otherwise a 0.
Now I have a stream of 1s and 0s, all I have to do is add them back up with .scan() and render it to the DOM.
What is the RxJS way to write this functionality?
EDIT: Method 1 — Build a stream of change-events
Based on #osln's answer.
/*** Helper Functions ***/
const showTyping = () => $('.typing').text('User is typing...');
const showIdle = () => $('.typing').text('');
const updateTimer = (x) => $('.timer').text(x);
const handleTypingStateChange = state =>
state === 1 ? showTyping() : showIdle();
/*** Program Logic ***/
const inputEvents$ = Rx.Observable.fromEvent($('#input'), 'input').share();
// streams to indicate when user is typing or has become idle
const typing$ = inputEvents$.mapTo(1);
const isIdle$ = inputEvents$.debounceTime(1000).mapTo(0);
// stream to emit "typing state" change-events
const typingState$ = Rx.Observable.merge(typing$, isIdle$)
.distinctUntilChanged()
.share();
// every second, sample from typingState$
// if user is typing, add 1, otherwise 0
const timer$ = Rx.Observable
.interval(1000)
.withLatestFrom(typingState$, (tick, typingState) => typingState)
.scan((a, b) => a + b, 0)
// subscribe to streams
timer$.subscribe(updateTimer);
typingState$.subscribe(handleTypingStateChange);
JSBin Live Demo
EDIT: Method 2 — Using exhaustMap to start counter when user starts typing
Based on Dorus' answer.
/*** Helper Functions ***/
const showTyping = () => $('.typing').text('User is typing...');
const showIdle = () => $('.typing').text('');
const updateTimer = (x) => $('.timer').text(x);
/*** Program Logic ***/
// declare shared streams
const inputEvents$ = Rx.Observable.fromEvent($('#input'), 'input').share();
const idle$ = inputEvents$.debounceTime(1000).share();
// intermediate stream for counting until idle
const countUntilIdle$ = Rx.Observable
.interval(1000)
.startWith('start counter') // first tick required so we start watching for idle events right away
.takeUntil(idle$);
// build clock stream
const clock$ = inputEvents$
.exhaustMap(() => countUntilIdle$)
.scan((acc) => acc + 1, 0)
/*** Subscribe to Streams ***/
idle$.subscribe(showIdle);
inputEvents$.subscribe(showTyping);
clock$.subscribe(updateTimer);
JSBin Live Demo
If you want to continuously update the UI, I don't think there's any way around using a timer - I might have written the stream a little differently by initiating the timer by change-events - but your current stream seems also okay as it is already:
const inputEvents$ = Rx.Observable
.fromEvent($('#input'), 'input');
const typing$ = Rx.Observable.merge(
inputEvents$.mapTo('TYPING'),
inputEvents$.debounceTime(1000).mapTo('IDLE')
)
.distinctUntilChanged()
.do(e => e === 'TYPING' ? showTyping() : showIdle())
.publishReplay(1)
.refCount();
const isTyping$ = typing$
.map(e => e === "TYPING");
const timer$ = isTyping$
.switchMap(isTyping => isTyping ? Rx.Observable.interval(100) : Rx.Observable.never())
.scan(totalMs => (totalMs + 100), 0)
.subscribe(updateTimer);
Live here.
If you don't need to update the UI and just want to capture the duration of the typing, you could use start- and stop-events and map them to timestamps like this e.g.:
const isTyping$ = typing$
.map(e => e === "TYPING");
const exactTimer$ = isTyping$
.map(() => +new Date())
.bufferCount(2)
.map((times) => times[1] - times[0])
.do(updateTimer)
.do(typedTime => console.log("User typed " + typedTime + "ms"))
.subscribe();
Live here.
I notice a few problems with your code. The gist of it is good, but if you use different operators you can do the same thing even easier.
First you use switchMap, this is a nice operator to start a new stream every time a new input arrives. However, what you really want is to continue the current timer as long as the user is typing. A better operator here would be exhaustMap because exhaustMap will keep the already active timer until it stops. We can then stop the active timer if the user is not typing for 1 second. That is easily done with .takeUntil(input.debounceTime(1000)). That would result in the very short query:
input.exhaustMap(() => Rx.Observable.timer(1000).takeUntil(input.debounceTime(1000)))
To this query, we can hook the display events you want, showTyping, showIdle etc. We also need to fix the timers index, as it will reset every time the user stops typing. This can be done with using the second parameter of project function in map, as this is the index of the value in the current stream.
Rx.Observable.fromEvent($('#input'), 'input')
.publish(input => input
.exhaustMap(() => {
showTyping();
return Rx.Observable.interval(1000)
.takeUntil(input.startWith(0).debounceTime(1001))
.finally(showIdle);
})
).map((_, index) => index + 1) // zero based index, so we add one.
.subscribe(updateTimer);
Notice i used publish here, but it is not strictly needed as the source is hot. However recommended because we use input twice and now we do not have to think about if it's hot or cold.
Live demo
/*** Helper Functions ***/
const showTyping = () =>
$('.typing').text('User is typing...');
const showIdle = () =>
$('.typing').text('');
const updateTimer = (x) =>
$('.timer').text(x);
/*** Program Logic ***/
Rx.Observable.fromEvent($('#input'), 'input')
.publish(input => input
.exhaustMap(() => {
showTyping();
return Rx.Observable.interval(1000)
.takeUntil(input.startWith(0).debounceTime(1001))
.finally(showIdle);
})
).map((_, index) => index + 1) // zero based index, so we add one.
.subscribe(updateTimer);
<head>
<script src="https://code.jquery.com/jquery-3.1.0.js"></script>
<script src="https://unpkg.com/#reactivex/rxjs#5.0.0-beta.12/dist/global/Rx.js"></script>
</head>
<body>
<div>
<div>Seconds spent typing: <span class="timer">0</span></div>
<input type="text" id="input">
<div class="typing"></div>
</div>
</body>