10

I'm trying to implement a "save on type" feature for a form using RxJS v5 beta.

The data should be posted to the backend as the user types into the text fields. I'm creating a Rx.Subject to fire new events (next()) for new user input and post it with HTTP requests.

I've used this question as a starting point: RxJS wait until promise resolved

However, with the solution from this post, simultaneous request to the backend are sent.

My goal is to only send one request and defer following requests until a running request has completed. After completion of the request the last of the pending events should be emitted (like it is the case in debounceTime)

The example function in the following snippet uses the approach from the linked SO question. This sends requests for all the input values.

The workaround function function uses a promise stored outside of the "stream" to block and wait for a previous request. This works and only sends a request for the last input value. But that seems to not follow the concept of RxJs and feels hacky.

Is there a way to achieve this with RxJS?

function fakeRequest(value) {
  console.log('start request:', value)
  return new Promise((resolve) => { 
    setTimeout(() => resolve(value), 1000);
 });
}

function example() {
  let subject = new Rx.Subject();
  
  subject
    .debounceTime(500)
    .switchMap(input => fakeRequest(input))
    .subscribe(data => console.log(data))

  subject.next('example value 1');
  subject.next('example value 2');
  subject.next('example value 3');
  subject.next('example value 4');
}


function workaround() {
  let subject = new Rx.Subject();

  let p = Promise.resolve();
  subject
    .debounceTime(500)  
    .switchMap(input => p.then(() => input))
    .do(input => p = fakeRequest(input))
    .subscribe(data => console.log(data))

  subject.next('workaround value 1');
  subject.next('workaround value 2');
  subject.next('workaround value 3');
  subject.next('workaround value 4');
}

example();
// workaround();
<script src="https://unpkg.com/@reactivex/rxjs@5.0.0-rc.2/dist/global/Rx.js"></script>
Community
  • 1
  • 1
Stefan
  • 14,530
  • 4
  • 55
  • 62
  • An operator similar to what you are looking for is discussed in this issue: https://github.com/ReactiveX/rxjs/issues/1777 – Christian Mar 05 '19 at 12:53

2 Answers2

10

If you want to run requests in order and not discard any of them then use concat() or concatMap() operators. These wait until the previous Observable completes and then continue with the next one.

function fakeRequest(value) {
  console.log('start request:', value)
  return new Promise((resolve) => { 
    setTimeout(() => resolve(value), 1000);
 });
}

let subject = new Subject();
subject.concatMap(value => Observable.fromPromise(fakeRequest(value)))
  .subscribe(value => console.log(value));

subject.next('example value 1');
subject.next('example value 2');
subject.next('example value 3');
subject.next('example value 4');

This prints to console:

start request: example value 1
example value 1
start request: example value 2
example value 2
start request: example value 3
example value 3
start request: example value 4
example value 4

See live demo: https://jsbin.com/xaluvi/4/edit?js,console

If you wanted to ignore values then debounce, throttle or audit are all good choices.

Edit: In newer RxJS versions you don't even need to use fromPromise (or from) and just return the Promise in concatMap.

martin
  • 93,354
  • 25
  • 191
  • 226
  • Thanks, this helped. But the problem I'm having now is that all the events that were blocked by the promise are emitted as soon as the promise is resolved. I'd actually only want the last one to emit (as it is the case with `debounceTime`, see my edit) – Stefan Nov 15 '16 at 14:09
  • @Stefan So what do you want? Emit all of them or just the last one? It sounds like you need `auditTime()` operator http://reactivex.io/rxjs/class/es6/Observable.js~Observable.html#instance-method-audit – martin Nov 15 '16 at 14:13
  • Emit just the last one. I was fiddling with `audit` but couldn't get it working. Feedback is very much appreciated. – Stefan Nov 15 '16 at 17:29
  • @Stefan I don't know what didn't work for you. There's also `auditTime()` maybe that one suits your needs better. – martin Nov 16 '16 at 08:10
3

You can create observable from a promise and use use delayWhen operator in order to wait until that observable emits (promise resolves).

import { timer } from 'rxjs';
import { delayWhen, from } from 'rxjs/operators';

const myPromise = new Promise(resolve => setTimeout(resolve, 3000)); // will resolve in 3 sec


const waitable = timer(0, 1000).pipe(delayWhen(() => from(myPromise)));
waitable.subscribe(x => console.log(x));

Each emit should be delayed by 3 seconds.

cuddlemeister
  • 1,586
  • 12
  • 15
  • This is also the way I am using when I would like to have a pseudo asynchronous "tap" operator. – Flavien Volken Oct 06 '21 at 13:01
  • @FlavienVolken consider using `observeOn(asyncScheduler)` instead, that is a proper way to make your synchronous stream async – cuddlemeister Jun 23 '22 at 06:31
  • Mmm… how would you do this ? tap is synchronous, if we need to wait for a side effect to happen before continuing the stream, then `delayWhen` is a good option. If I use `observeOn`, I might end up with another scheduler for my stream, but the tap will still be synchronous. – Flavien Volken Jun 23 '22 at 09:29
  • @FlavienVolken `of(1).pipe(observeOn(asyncScheduler), tap(.. /* logic here will execute in the next macrotask */).subscribe()` – cuddlemeister Jun 24 '22 at 10:19