Making a typing timer in RxJS; Tracking time spent typing - javascript

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>

Related

How to change state immediately and then change back to empty after a delay using UseState?

I have a function where I'd like to set the successMessage to "success" and then after 5 seconds I'd like to set successMessage to an empty string again, but I haven't been able to figure it out.
Preferably I'd like to create a helper function where I can pass in 3 variables - function to run before (setSuccessMessage('Success!')), function to run after (setSuccessMessage('')) and the delay (5000ms)
How can I achieve this?
The Simpliest solution that runs timeout whenever the message changes to anything but empty string. Bear in mind that this example is written by hand without any testing ;)
const [successMessage, setSuccessMessage] = useState('');
const timerRef = useRef(null);
const resetMessage = () => {
clearTimeout(timerRef.current)
timerRef.current = setTimeout(() => setSuccessMessage(''), 5000)
}
useEffect(() => {
if (successMessage !== '') resetMessage();
}, [successMessage, resetMessage]);

RxJS - How to share the output of an expensive observable but rerun that observable if its requested again after N seconds?

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.

Reactive Extensions Buffer on count, interval and event

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

How to forEach over a whole stream after a set interval in RxJS

I have a stream of zip codes that I want to iterate over after a set interval. I'm trying to update the temperature that is shown on the page by sending the zip codes and getting the data back from the API. I have to send one at a time. So I need to be able to get all the distinct zip codes after a certain interval and iterate over the whole stream. Then I want to update the temperature on the page.
// Get stream of zip codes
const zipcodeStream =
Rx.Observable
.fromEvent(zipcodeInput, 'input')
.map(e => e.target.value)
.filter(zip => zip.length === 5);
// Create a timer to refresh the data
Rx.Observable
.interval(5000)
.zip(zipcodeStream, ([i, zip]) => zip)
.forEach((...args) => {
console.log('interval forEach args', ...args);
});
This only sends a single zip code when a new zip code is entered and the interval has passed. I want access to them all to iterate over.
Since you want to save all the item emitted by zipcodeStream so you can iterate over them every 5 seconds, you'll need to use a ReplaySubject. These save all items emitted, and replay them whenever an Observer subscribes.
In contrast, your current zipcodeStream Observable is a "hot" observable. This means that it starts emitting items as soon as it's created, and any subsequent subscribers will 'miss' any items emitted before they subscribed.
const zipcodeStream =
Rx.Observable
.fromEvent(zipcodeInput, 'input')
.map(e => e.target.value)
.filter(zip => zip.length === 5);
const zipcodeSubject = new Rx.ReplaySubject();
const zipcodeDisposable = zipcodeStream.subscribe(zipcodeSubject);
Rx.Observable
.interval(5000)
// Every 5000ms, will emit all items from zipcodeSubject
.flatMapLatest(() => zipcodeSubject)
.forEach((...args) => {
console.log('interval forEach args', ...args);
});

Repeating/Resetting an observable

I am using rxjs to create a "channel nummber" selector for a remote control on a smart tv. The idea being that as you are entering the numbers you would see them on the screen and after you have finished entering the numbers, the user would would actually be taken to that channel.
I use two observables to achieve this:
A "progress" stream that listens to all number input and emits the concatenated number string out as the numbers are inputed via the scan operator.
A "completed" stream that, after n milliseconds of no number having being entered, would emit the final numeric string as completed. EG: 1-2-3 -> "123".
Here is the code that I use to try and solve this:
channelNumber:
module.exports = function (numberKeys, source, scheduler) {
return function (completedDelay) {
var toNumericString = function (name) {
return numberKeys.indexOf(name).toString();
},
concat = function (current, numeric) {
return current.length === 3 ? current : current + numeric;
},
live = createPress(source, scheduler)(numberKeys)
.map(toNumericString)
.scan(concat, '')
.distinctUntilChanged(),
completed = live.flatMapLatest(function (value) {
return Rx.Observable.timer(completedDelay, scheduler).map(value);
}),
progress = live.takeUntil(completed).repeat();
return {
progress: progress,
completed: completed
};
};
};
createPress:
module.exports = function (source, scheduler) {
return function (keyName, throttle) {
return source
.filter(H.isKeyDownOf(keyName))
.map(H.toKeyName);
};
};
createSource:
module.exports = function (provider) {
var createStream = function (event) {
var filter = function (e) {
return provider.hasCode(e.keyCode);
},
map = function (e) {
return {
type: event,
name: provider.getName(e.keyCode),
code: e.keyCode
};
};
return Rx.Observable.fromEvent(document, event)
.filter(filter)
.map(map);
};
return Rx.Observable.merge(createStream('keyup'), createStream('keydown'));
};
Interestingly the above code, under test conditions (mocking source and scheduler using Rx.TestScheduler) works as expected. But in production, when the scheduler is not passed at all and source is the result of createPress (above), the progress stream only ever emits until complete, and then never again. It's as if the repeat is completely ignored or redundant. I have no idea why.
Am I missing something here?
You can use Window. In this case, I would suggest WindowWithTime. You can also do more interesting things like use Window(windowBoundaries) and then pass the source with Debounce as boundary.
source
.windowWithTime(1500)
.flatMap(ob => ob.reduce((acc, cur) => acc + cur, ""))
Also, since our windows are closed observables, we can use Reduce to accumulate the values from the window and concat our number.
Now, this variant will close after 1,5 second. Rather, we would want to wait x seconds after the last keypress. Naïve we could do source.window(source.debounce(1000)) but now we subscribe to our source twice, that's something we want to avoid for two reasons. First we do not know is subscribing has any side effects, second we do not know the order subscriptions will receive events. That last thing isn't a problem since we use debounce that already adds a delay after the last keypress, but still something to consider.
The solution is to publish our source. In order to keep the publish inside the sequence, we wrap it into observable.create.
Rx.Observable.create(observer => {
var ob = source.publish();
return new Rx.CompositeDisposable(
ob.window(ob.debounce(1000))
.subscribe(observer),
ob.connect());
}).flatMap(ob => ob.reduce((acc, cur) => acc + cur, ""))
Edit: Or use publish like this:
source.publish(ob => ob.window(ob.debounce(1000)))
.flatMap(ob => ob.reduce((acc, cur) => acc + cur, ""))

Categories