简体   繁体   中英

RxJS Operator to compose observables into state?

I am currently facing a pretty common problem, when it comes to asynchronous execution, state handling and the indentation hell.

Let's say there is a REST call that requests adding user information ( user_info ) to a user, notify his contacts of that change and return the inserted or modified user information in the response. But a user object does only know all the ids of his user_info objects via 1 to n relation.

Call Sequence:

request -> saveUserInfo -> updateUser -> notifyContacts -> response

saveToDb , updateUser and notifyContacts are all functions that perform an asynchronous side effect and can not simply be composed as they all have different inputs and outputs and depend on the order of execution.

Here is a loose representation of the function headers

function saveUserInfo(payload): Promise<UserInfo[]> { /*...*/ }
function updateUser(user_id, user_infos): Promise<User> { /*...*/ }
function sendNotification(user, user_infos): Promise<void> { /*...*/ }

To write this request handler, I was currently working a lot with mergeMap to subscribe to the inner observables that performed the current asynchronous action. It looked something like this:

function handleRequest(payload, user_id) {
  return saveUserInfo(payload).pipe(
    mergeMap(user_infos =>
      from(updateUser(user_id, user_infos)).pipe(
        mergeMap(user =>
          from(notifyContacts(user, user_infos)).pipe(map(() => user_infos))
        )
      )
    )
  )
}

I was not satisfied with this solution, because there will be more logic added to this in the future and I know this can become cumbersome at some point.

So I searched through the RxJS documentation and sadly did not find a clever way to pipe these asynchronous calls together in a much more pleasing way. The biggest issue is, that I am not really able to completely formulate the problem here in a few words, which might have lead me to write this question.

Does anyone know a better solution to this? I am sure there must be something!

Preferably without helper funtions and in pure RxJS.

As I could not find anything helpful on the internet, I came up with a higher order function to provide me with a pipeable operator, that could potentially solve my problem, I called it sourceMap .

It should subscribe to an inner observable via mergeMap and merge the result of it into a keyed state object.

const sourceMap = <S, T extends Record<string, any>>(
  predicate: (acc: T) => Promise<S> | Observable<S>,
  key?: keyof T
) => {
  return mergeMap((acc: T) =>
    from(predicate(acc)).pipe(map(res => (key ? { ...acc, [key]: res } : acc)))
  )
}

Implementation looks like this:

function handleRequest(payload, user_id): Observable<UserInfo[]> {
  const state$ = of({ user_infos: [] as UserInfo[], user: {} as User })

 return state$.pipe(
    sourceMap(() => saveUserInfo(user_id, payload), 'user_info'),
    sourceMap(({ user_info }) => updateUser(user_id, user_info), 'user'),
    sourceMap(({ user_info, user }) => notifyContacts(user, user_info)),
    pluck('user_info')
  )
}

Still looking for a better solution to this!

When you're using a high order mapping operator(eg mergeMap and its friends) and if the provided callback returns a promise , you don't have to do () => from(promise) . mergeMap will automatically do it for you.

With this in mind, I'd change this:

function handleRequest(payload, user_id) {
  return saveUserInfo(payload).pipe(
    mergeMap(user_infos =>
      from(updateUser(user_id, user_infos)).pipe(
        mergeMap(user =>
          from(notifyContacts(user, user_infos)).pipe(map(() => user_infos))
        )
      )
    )
  )
}

into this

function handleRequest(payload, user_id) {
  return from(saveUserInfo(payload)) // `saveUserInfo(payload)` returns a promise
    .pipe(
      mergeMap(user_infos => forkJoin(of({ user_infos }), updateUser(user_id, user_infos)),
      mergeMap(([acc, user]) => forkJoin(of({ ...acc, otherPropsHere: true }), notifyContacts(user, user_infos))),
      map(([acc]) => acc['user_infos'])
    ),
  )
}

forkJoin will emit an array after all its provided ObservableInput s emitted at least one value and completed.

As you can see, the first element of the returned array is the accumulator.

You can also do it like this:

function saveUserInfo(payload: Payload): Promise<UserInfo[]>;
function updateUser(user_id: string, user_infos: UserInfo[]): Promise<User>;
function sendNotification(user: User, user_infos: UserInfo[]): Promise<void>;

function handleRequest(payload: Payload, user_id: string): Observable<UserInfo[]> {
  return from(saveUserInfo(payload)).pipe(
    switchMap(user_infos => updateUser(user_id, user_infos).then(user => ({ user, user_infos }))),
    switchMap(({ user, user_infos }) => sendNotification(user, user_infos).then(() => user_infos))
  );
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM