1

I'm building an Angular2 app, so I'm getting used to Observables and Reactive Extensions as a whole. I'm using TypeScript and rxjs.

Now I've got an observable, or a stream if you will, of an array of some objects. Let's say Person-objects. Now I've got two other streams of Person-objects and want to combine these so I get a stream which is always up to date:

var people$ = getPeople();                  // Observable<Person[]>
var personAdded$ = eventHub.personAdded;    // Observable<Person>;
var personRemoved$ = eventHub.personRemoved // Observable<Person>;

var allwaysUpToDatePeople$ = people$.doSomeMagic(personAdded$, personRemoved$, ...);

If the people-stream emits an array of, let's say, 5 people, and after that the personAdded-stream emits a person, the allPeople-stream wil emit an array of 6. If the personRemoved-stream emits a person, the allPeople-stream should emit an array of Person-objects without the one just emitted by the personRemoved-stream.

Is there a way built into rxjs to get this behaviour?

YentheO
  • 307
  • 4
  • 13

2 Answers2

2

My suggestion is that you wrap the idea of an action into a stream which can then be merged and applied directly to the Array.

The first step is to define some functions that describe your actions:

function add(people, person) {
  return people.concat([people]);
}

function remove(people, person) {
  const index = people.indexOf(person);
  return index < 0 ? people : people.splice(index, 1);
}

Note: we avoid mutating the Array in place because it can have unforeseen side effects. Purity demands that we create a copy of the array instead.

Now we can use these functions and lift them into the stream to create an Observable that emits functions:

const added$ = eventHub.personAdded.map(person => people => add(people, person));
const removed$ = eventHub.personRemoved.map(person => people => remove(people, person));

Now we get events in the form of: people => people where the input and output will be an array of people (in this example simplified to just an array of strings).

Now how would we wire this up? Well we really only care about adding or removing these events after we have an array to apply them to:

const currentPeople = 

  // Resets this stream if a new set of people comes in
  people$.switchMap(peopleArray => 

    // Merge the actions together 
    Rx.Observable.merge(added$, removed$)

      // Pass in the starting Array and apply each action as it comes in
      .scan((current, op) => op(current), peopleArray)

      // Always emit the starting array first
      .startWith(people)
  )
  // This just makes sure that every new subscription doesn't restart the stream
  // and every subscriber always gets the latest value
  .shareReplay(1);

There are several optimizations of this technique depending on your needs (i.e. avoiding the function currying, or using a binary search), but I find the above relatively elegant for the generic case.

hjing
  • 4,922
  • 1
  • 26
  • 29
paulpdaniels
  • 18,395
  • 2
  • 51
  • 55
  • Nice explanation, I'm gonna try this out, but I get what you're saying! I would think this is something people want to do more often, not? – YentheO Nov 06 '16 at 15:14
1

You want to merge all of the streams (Ghostbusters style) and then use the scan operator to figure out the state. The scan operator works like Javascript reduce.

Here's a demo...

const initialPeople = ['Person 1', 'Person 2', 'Person 3', 'Person 4'];

const initialPeople$ = Rx.Observable.from(initialPeople);

const addPeople = ['Person 5', 'Person 6', 'Person 7'];

const addPeople$ = Rx.Observable.from(addPeople)
            .concatMap(x => Rx.Observable.of(x).delay(1000)); // this just makes it async

const removePeople = ['Person 2x', 'Person 4x'];

const removePeople$ = Rx.Observable.from(removePeople)
                                              .delay(5000)
                                                .concatMap(x => Rx.Observable.of(x).delay(1000));

const mergedStream$ = Rx.Observable.merge(initialPeople$, addPeople$, removePeople$)

mergedStream$
  .scan((acc, stream) => {
        if (stream.includes('x') && acc.length > 0) {
            const index = acc.findIndex(person => person === stream.replace('x', ''))
            acc.splice(index, 1);
        } else {
            acc.push(stream);
        }
      return acc;
  }, [])
  .subscribe(x => console.log(x))

// In the end, ["Person 1", "Person 3", "Person 5", "Person 6", "Person 7"]

http://jsbin.com/rozetoy/edit?js,console

You didn't mention your data's structure. My use of "x" as a flag is a little (lot) clunky and problematic. But I think you see how you can modify the scan operator to fit your data.

D. Walsh
  • 1,963
  • 1
  • 21
  • 23