Zabawa Koralikami

Łukasz Karpuć, Marzec/Kwiecień 2019

Czym jest RxJS?

RxJS, czyli...

 

  • API wspomagające programowanie reaktywne
  • podejście do problemu Producenta i Konsumenta
  • implementacja wzorca Obserwatora
  • członek rodziny ReactiveX (Extensions)

Programowanie reaktywne

Programowanie reaktywne

onClick() {
    makeSquareRed();
    makeCirleRed();

}

onMouseOver() {
    makeSquareGreen();
    makeCircleGreen();

}

onKeypress() {
    makeSquareBlue();
    makeCirleBlue();

}

Programowanie reaktywne

onClick() {
    makeSquareRed();
    makeCirleRed();
    makeTriangleRed();
}

onMouseOver() {
    makeSquareGreen();
    makeCircleBlue();
    makeTriangleGreen();
}

onKeypress() {
    makeSquareBlue();
    makeCirleBlue();
    makeTriangleBlue();
}

Programowanie reaktywne

onClick() {

    color = 'red'

}

onMouseOver() {

    color = 'green'

}

onKeypress() {

    color = 'blue'

}

setInterval(() => renderAll(), 1000)

Programowanie reaktywne

onClick() {

    color = 'red'

}

onMouseOver() {

    color = 'green'

}

onKeypress() {

    color = 'blue'

}

observeColor(() => renderAll())

Producenci i Konsumenci

Producenci i Konsumenci

Producenci i Konsumenci

SABRE

PROGRAMISTA

$$$$$$$

PUSH

Producenci i Konsumenci

PROGRAMISTA

PULL

URZĄD
SKARBOWY

$$$

Wzorzec Obserwatora

*żródło: https://sourcemaking.com
*żródło: https://sourcemaking.com

ReactiveX

RxJS

interface Observer<T> {
  closed?: boolean;
  next: (value: T) => void;
  error: (err: any) => void;
  complete: () => void;
}

interface Subscribable<T> {
    subscribe(observer: Observer<T>): Subscription;
}

interface Observable<T> extends Subscribable<T> {
    new (observer: Observer<T>): this;
}

interface Subject<T, R> extends Observer<T>, Subscribable<R> {
}

RxJS

interface Observer<T> {
  
    // REACTS to data changes


}

interface Subscribable<T> {
    // is a SOURCE of data
}

interface Observable<T> extends Subscribable<T> {
    // DELIVERS data; is mostly stateless
}

interface Subject<T, R> extends Observer<T>, Subscribable<R> {
    // PROPAGATES data; is mostly stateful
}

Observable vs Subject

Observable

Jest źródłem danych.

 

Nie jest odbiorcą danych.

 

Nie posiada stanu.

 

Każda subskrypcja powoduje ponowne przetworzenie kolekcji.

Subject

Jest źródłem danych.

 

Jest odbiorcą danych.

 

Posiada stan.

 

Subskrypcje są z nim powiązane. Nie muszą otrzymywać tych samych danych.

RxJS

// Observable example

const observable = new Observable(subscriber => {
  subscriber.next(1);
  subscriber.next(2);
  subscriber.next(3);

  setTimeout(() => {
    subscriber.next(4);
    subscriber.complete();
  }, 1000);
});
 
console.log('just before subscribe');

observable.subscribe({
  next(x) { console.log('got value ' + x); },
  error(err) { console.error('something wrong occurred: ' + err); },
  complete() { console.log('done'); }
});

console.log('just after subscribe');

// just before subscribe
// got value 1
// got value 2
// got value 3
// just after subscribe
// got value 4
// done

RxJS

// Subject example

const subject = new ReplaySubject(3); // buffer 3 values for new subscribers
 
subject.subscribe({
  next: (v) => console.log(`observerA: ${v}`)
});
 
subject.next(1);
subject.next(2);
subject.next(3);
subject.next(4);
 
subject.subscribe({
  next: (v) => console.log(`observerB: ${v}`)
});
 
subject.next(5);
 
// Logs:
// observerA: 1
// observerA: 2
// observerA: 3
// observerA: 4
// observerB: 2
// observerB: 3
// observerB: 4
// observerA: 5
// observerB: 5

RxJS

// map operator example

//emit ({name: 'Joe', age: 30}, {name: 'Frank', age: 20},{name: 'Ryan', age: 50})
const source = from([
  { name: 'Joe', age: 30 },
  { name: 'Frank', age: 20 },
  { name: 'Ryan', age: 50 }
]);

//grab each persons name, could also use pluck for this scenario
const example = source.pipe(
    map(({ name }) => name)
);

//output: "Joe","Frank","Ryan"
const subscribe = example.subscribe(val => console.log(val));

RxJS

// filter example

//emit every second
const source = interval(1000);

//filter out all values until interval is greater than 5
const example = source.pipe(
    filter(num => num > 5)
);

/*
  "Number greater than 5: 6"
  "Number greater than 5: 7"
  "Number greater than 5: 8"
  "Number greater than 5: 9"
*/
const subscribe = example.subscribe(val =>
  console.log(`Number greater than 5: ${val}`)
);

RxJS

// scan example (continuous reduce)

const subject = new Subject();

//scan example building an object over time
const example = subject.pipe(
  scan((acc, curr) => Object.assign({}, acc, curr), {})
);

//log accumulated values
const subscribe = example.subscribe(val => console.log('Accumulated object:', val));

//next values into subject, adding properties to object
// {name: 'Joe'}
subject.next({ name: 'Joe' });

// {name: 'Joe', age: 30}
subject.next({ age: 30 });

// {name: 'Joe', age: 30, favoriteLanguage: 'JavaScript'}
subject.next({ favoriteLanguage: 'JavaScript' });

RxJS

// custom operator example

let store = sidepanelService.getStore(); // Redux Store
let store$ = Rx.Observable.from(store); // Store's state update

let mutations$ = store$
    .pipe(observeMutation(KeyValueMutation.calculateMutation)); // Store's state mutation

mutations$
    .pipe(filter(it => it.mutatedKeys.visibleItem)) // Mutation of given attribute
    .subscribe(it => this.getSidepanelWidget()._setVisibleItemKey(it.new.visibleItem));

mutations$
    .pipe(filter(it => mutation.mutatedKeys.isWideMode)) // Mutation of given attribute
    .subscribe(it => this.getSidepanelWidget()._handleWideModeChange(it.new.isWideMode));

Jakie są korzyści?

Zapełniamy lukę

Zapełniamy lukę

pull push
skalar function() Promise
kolekcja function*()

Zapełniamy lukę

pull push
skalar function() Promise
kolekcja function*()       bservable

Uzgadniamy język

Uzgadniamy język

- Czemu Pan ukradł to piwo?

- Herr Wysoki Sądzie, ja nic nie ukradłem... Pisało "bier", to wziąłem!

Tworzymy wspólny ekosystem

Tworzymy wspólny ekosystem

 

  • uniwersalne operatory
  • uniwersalne narzędzia

Używamy znanych funkcji/metod

Używamy znanych funkcji/metod

 

  • map()
  • filter()
  • reduce()
  • zip()
  • concat()
  • etc.

Używamy znanych funkcji/metod

 

Dobre katalogi operatorów na: 

  • https://www.learnrxjs.io
  • https://rxmarbles.com

 

Jak to testować?

jest + rxjs-marbles

it('test with jest only', (done) => {
    // given
    let source = from([1, 2, 3, 4]);
    let expected = [2, 4, 6, 8];

    // when
    let result = source.pipe(map(it => it * 2));

    result.subscribe(function(value) {

        // then
        expect(value).toEqual(expected.shift());

        if( !expected.length ) {
            done();
        }
    });
});

jest + rxjs-marbles

it('test with rxjs-marbles', marbles(m => {
    let numbers = [...new Array(10).keys()];

    // given
    let source = m.hot(     '-0-1-2-3-|', numbers);
    let subs =              '^--------!';
    let expected = m.cold(  '-0-2-4-6-|', numbers);

    // when
    let result = source.pipe(map(it => it * 2));

    // then
    m.expect(result).toBeObservable(expected);
    m.expect(source).toHaveSubscriptions(subs);
}));

jest + rxjs-marbles

it('test with jasmine-marbles', () => {
    let numbers = [...new Array(10).keys()];

    // given
    let source = jm.hot(    '-0-1-2-3-|', numbers);
    let subs =              '^--------!';
    let expected = jm.cold( '-0-2-4-6-|', numbers);

    // when
    let result = source.pipe(map(it => it * 2));

    // then
    expect(result).toBeObservable(expected);
    expect(source).toHaveSubscriptions(subs);
});

jasmine-marbles

jest + rxjs-marbles

it('test with jasmine-marbles', () => {
    let numbers = [...new Array(10).keys()];

    // given
    let source = jm.hot(    '-0-1-2-3-|', numbers);

    let expected = jm.cold( '-0-2-4-6-|', numbers);

    // when
    let result = source.pipe(map(it => it * 2));

    // then
    expect(result).toBeObservable(expected);

});

jasmine-marbles

jest + rxjs-marbles

it('test with rxjs/testing', () => {
    let scheduler = new TestScheduler((actual, expected) => 
        expect(actual).toEqual(expected));

    scheduler.run(m => {
        let numbers = [...new Array(10).keys()];

        // given
        let source = m.hot(     '-0-1-2-3-|', numbers);
        let subs =              '^--------!';
        let expected =          '-0-2-4-6-|';

        // when
        let result = source.pipe(map(it => it * 2));

        // then
        m.expectObservable(result, subs).toBe(expected, numbers);
        m.expectSubscriptions(result.subscriptions).toBe(subs);
    });
});

/testing ?

jest + rxjs-marbles

it('test with rxjs/testing', () => {
    let scheduler = new TestScheduler((actual, expected) => 
        expect(actual).toEqual(expected));

    scheduler.run(m => {
        let numbers = [...new Array(10).keys()];

        // given
        let source = m.hot(     '-0-1-2-3-|', numbers);
        let subs =              '^--------!';
        let expected =          '-0-2-4-6-|';

        // when
        let result = source.pipe(map(it => it * 2));

        // then
        m.expectObservable(result, subs).toBe(expected, numbers);
        m.expectSubscriptions(result.subscriptions).toBe(subs);
    });
});

/testing ?

Składnia koralikowa

abo Marble Syntax

Składnia koralikowa

Składnia koralikowa

-1---2-----3-4---5-|
---A--B--CD--------|
---X-YZ--STR-W---U-|
{ X: '1A', Y: '2A', Z: '2B', /* ... */ U: '5D' }

Składnia koralikowa

-

----------

Wirtualna ramka czasu

Hot Observable / Cold Observable / Subskrypcje

Składnia koralikowa

[0-9]+[ms|s|m]

- 8ms -

Upływ czasu

Hot Observable / Cold Observable / Subskrypcje

Składnia koralikowa

|

- 8ms -|

Koniec kolekcji (.complete())

Hot Observable / Cold Observable / Subskrypcje

Składnia koralikowa

#

- 8ms -#

Błąd (.error())

Hot Observable / Cold Observable / Subskrypcje

Składnia koralikowa

[a-z0-9]

- 8ms -a-b-c-|

Kolejna wartość (.next())

Hot Observable / Cold Observable / Subskrypcje

Składnia koralikowa

()

- 8ms -a-b-(c|)

Zdarzenia równoległe

Hot Observable / Cold Observable / Subskrypcje

Składnia koralikowa

^
^ 8ms ----------
- 8ms -a^b-(c|)

Początek subskrypcji (.subscribe())

Hot Observable / Cold Observable / Subskrypcje

Składnia koralikowa

!

^ 8ms --------!

Koniec subskrypcji (.unsubscribe())

Hot Observable / Cold Observable / Subskrypcje

Składnia koralikowa

  const source = hot('--a--a--a--a--a--a--a--');

  const sub1 = '      --^-----------!';
  const sub2 = '      ---------^--------!';

  const expect1 = '   --a--a--a--a--';
  const expect2 = '   -----------a--a--a-';

  expectObservable(source, sub1).toBe(expect1);
  expectObservable(source, sub2).toBe(expect2);

Przykład z dokumentacji

Hot vs Cold

Observable

Hot

Nie jest bezpośrednim źródłem danych. 

 

Producent najpewniej żyje już gdzieś w systemie.

 

Dlatego dla tego rodzaju observable'a można zaznaczyć moment rozpoczęcia subskrypcji.

Cold

Jest bezpośrednim źródłem danych.

 

Tworzy producenta (w zasadzie sam observable nim jest).

That's all folks!

Post Scriptum

 

  • http://reactivex.io/
  • https://github.com/ReactiveX/rxjs
  • https://www.learnrxjs.io
  • https://rxmarbles.com
  • https://github.com/cartant/rxjs-marbles
  • lukasz.karpuc@sabre.com

Zabawa Koralikami

By Lukasz K

Zabawa Koralikami

  • 341