# src / mixin / setup.js
Copyright (c) 2018 Florian Klampfer <https://qwtel.com/>

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>.


```js

import { Subject, defer, fromEvent, merge, NEVER } from "rxjs/_esm5";

import {
  catchError,
  tap,
  distinctUntilChanged,
  filter,
  map,
  mapTo,
  partition,
  pairwise,
  share,
  startWith,
  switchMap,
  takeUntil,
  withLatestFrom,
} from "rxjs/_esm5/operators";

import { URL } from "../url";

import { HINT, PUSH, POP } from "./constants";
import { unsubscribeWhen } from "./operators";

import { helperMixin } from "./methods";
import { historyMixin } from "./history";
import { fetchMixin } from "./fetching";
import { updateMixin } from "./update";
import { eventMixin } from "./events";
import { eventListenersMixin } from "./event-listeners";

export const setupObservablesMixin = C =>
  class extends eventListenersMixin(
    eventMixin(updateMixin(fetchMixin(historyMixin(helperMixin(C)))))
  ) {
```

A compare function for contexts, used in combination with `distinctUntilChanged`.
We use `cacheNr` as it is a convenient (hacky) way of circumventing
`distinctUntilChanged` when retrying requests.


```js
    compareContext(p, q) {
      return p.url.href === q.url.href && p.error === q.error && p.cacheNr === q.cacheNr;
    }
```

### Setup observable
This functions sets up the core observable pipeline of this component.


```js
    setupObservables() {
      this.cacheNr = 1;
```

For now, we take for granted that we have a stream of all `PUSH` events (loading a new page by
clicking on a link) and `HINT` events (probable click on a link) which are `pushSubject` and
`hintSubject` respectively.


```js
      this.pushSubject = new Subject();
      this.hintSubject = new Subject();
```

TODO: doc


```js
      const push$ = this.pushSubject.pipe(
        takeUntil(this.subjects.disconnect),
        map(event => ({
          type: PUSH,
          url: new URL(event.currentTarget.href, this.href),
          anchor: event.currentTarget,
          event,
          cacheNr: this.cacheNr,
        })),
        filter(this.isPushEvent.bind(this)),
        tap(({ event }) => {
          event.preventDefault();
          this.saveScrollPosition();
        })
      );
```

In additon to `HINT` and `PUSH` events, there's also `POP` events, which are caused by
modifying the browser history, e.g. clicking the back button, etc.


```js
      const pop$ = fromEvent(window, "popstate").pipe(
        takeUntil(this.subjects.disconnect),
        filter(() => window.history.state && window.history.state[this.histId()]),
        map(event => ({
          type: POP,
          url: new URL(window.location, this.href),
          event,
          cacheNr: this.cacheNr,
        }))
      );

      const reload$ = this.reload$.pipe(takeUntil(this.subjects.disconnect));
```

TODO: doc


```js
      const [hash$, page$] = merge(push$, pop$, reload$)
        .pipe(
          startWith({ url: new URL(this.initialHref) }),
```

HACK: make hy-push-state mimic window.location?


```js
          tap(({ url }) => (this._url = url)),
          pairwise(),
          share(),
          partition(this.isHashChange)
        )
        .map(obs$ =>
          obs$.pipe(
            map(([, x]) => x),
            share()
          )
        );
```

TODO: doc


```js
      const hint$ = this.subjects.prefetch.pipe(
        switchMap(prefetch => {
          if (!prefetch) return NEVER;
```

We don't want to prefetch (i.e. use bandwidth) for a _possible_ page load,
while a _certain_ page load is going on.
The `pauser$` observable let's us achieve this.
Needs to be deferred b/c of "cyclical" dependency.


```js
          const pauser$ = defer(() =>
```

A page change event means we want to pause prefetching, while
a response event means we want to resume prefetching.


```js
            merge(page$.pipe(mapTo(true)), this.fetch$.pipe(mapTo(false)))
          )
```

Start with `false`, i.e. we want the prefetch pipelien to be active


```js
            .pipe(startWith(false), share());

          return this.hintSubject.pipe(
            takeUntil(this.subjects.disconnect),
            unsubscribeWhen(pauser$),
            map(event => ({
              type: HINT,
              url: new URL(event.currentTarget.href, this.href),
              anchor: event.currentTarget,
              event,
              cacheNr: this.cacheNr,
            })),
            filter(this.isHintEvent.bind(this))
          );
        })
      );
```

The stream of (pre-)fetch events.
Includes definitive page change events do deal with unexpected page changes.


```js
      const prefetch$ = merge(hint$, page$).pipe(
```

Don't abort a request if the user "jiggles" over a link


```js
        distinctUntilChanged(this.compareContext.bind(this)),
        switchMap(this.makeRequest.bind(this)),
```

Start with some value so `withLatestFrom` below doesn't "block"


```js
        startWith({ url: {} }),
        share()
      );
```

TODO: doc


```js
      this.fetch$ = page$.pipe(
        tap(context => {
          this.updateHistoryState(context);
          this.onStart(context);
        }),
        withLatestFrom(prefetch$),
        switchMap(this.getResponse.bind(this, prefetch$)),
        share()
      );
```

TODO: doc


```js
      const [fetchOk$, fetchError$] = this.fetch$.pipe(partition(({ error }) => !error));
```

TODO: doc


```js
      const main$ = fetchOk$.pipe(
        map(this.responseToContent.bind(this)),
        tap(context => {
          this.onReady(context);
          this.updateDOM(context);
          this.onAfter(context);
          this.manageScrollPostion(context);
        }),
        tap({ error: e => this.onDOMError(e) }),
        catchError((e, c) => c),
```

If the experimental script feature is enabled,
scripts tags have been stripped from the content,
and this is where we insert them again.


```js
        switchMap(this.reinsertScriptTags.bind(this)),
        tap({ error: e => this.onError(e) }),
        catchError((e, c) => c)
      );
```

#### Subscriptions
Subscribe to main observables.


```js
      main$.subscribe(this.onLoad.bind(this));
```

Subscribe to hash observables.


```js
      hash$.subscribe(context => {
        this.updateHistoryStateHash(context);
        this.manageScrollPostion(context);
      });
```

Subscribe to the fetch error branch.


```js
      fetchError$.subscribe(this.onNetworkError.bind(this));
```

Fire `progress` event when fetching takes longer than expected.


```js
      page$
        .pipe(
          switchMap(context =>
            defer(() => this.animPromise).pipe(takeUntil(this.fetch$), mapTo(context))
          )
        )
        .subscribe(this.onProgress.bind(this));
```

TODO: doc


```js
      this.setupEventListeners();
    }
  };
```


