angular - Anpassad RxJS-operator fungerar inte korrekt


Jag har försökt att refaktorera ett kodavsnitt till en egen rxjs-operator, men jag kan inte få det att fungera.

Detta är min anpassade operator hittills:

export const isLastItemTheSame = (oldValue: any[], key: string, condition: boolean) => {

  return condition ? <T>(obsv: Observable<T[]>) => obsv.pipe(
    filter(newValue => {

      try {
        return (oldValue[oldValue.length - 1][key] === newValue[newValue.length - 1][key]);
      }
      catch(err) {
        return false;
      }
    }),
    mapTo(true)
  ) : <T>(obsv: Observable<T>) => obsv.pipe(ignoreElements());
};

Idén här är att jämföra en ny lista med en gammal lista, om den sista posten i båda listorna matchar, bör framgångsåteruppringningen för subscribe inte köras. Men om posterna inte matchar, bör den göra det.

De problem jag har just nu är:

  • <T>(obsv: Observable<T>) => obsv.pipe(ignoreElements())-delen fungerar inte eftersom den inte utlöser framgångs-callbacken.
  • När condition är true, returnerar operatorn ett booleanvärde istället för den nya listan. Detta gör det omöjligt att binda den nya listan till this.items i subscribe-success-callbacken.

Jag använder det så här:

const source$ = this.api.get<CustomResponse>('events');

source$.pipe(
  first(),
  tap((res) => this.total = res.totalsize || 0),
  map((res) => res.list),
  isLastItemTheSame(this.items, 'eventid', this.items.length && !isReset)
).subscribe((items: IEvent[]) => {

    // if (this.items.length && !isReset) {

    //   if (items[items.length - 1].eventid === this.items[this.items.length - 1].eventid) {
    //     return;
    //   }
    // }

    this.items = isReset ? items : [...this.items, ...items];
  }, (err) => {

    if (err.status !== 401) {

      this.router.navigate(['dashboard']).then(() => {
        this.notifications.newNotification({message: this.translate.instant('NOTIFICATIONS.EVENTS.GET_LIST_ERROR'), theme: 'danger'});
      });
    }
  }
);

Koden som har kommenterats ut är koden som jag försöker refaktorera, vilket gör det lätt att se vad jag försöker uppnå.

Hur kan jag fixa detta?

Svar #1

Redigera: Detta svar förutsätter att du faktiskt vill jämföra mot en extern referenslista snarare än bara det tidigare avgivna värdet av samma observerbara. Det är vad din fråga angav, men om du menar att referera till det tidigare värdet, ta en titt på mitt andra svar.

Låt oss göra några observationer först:

När villkoret är sant returnerar operatorn en boolean istället för den nya listan.

Det är vad din mapTo(true) gör, så ta bara bort det :-)

ignoreElements())-delen fungerar inte eftersom den inte utlöser framgångs-callbacken.

Det beror på att du inte vill ignorera elementen, utan bara returnera det observerbara oförändrat i det här fallet.

Jag skulle dock inte flytta villkoret in i operatorn - det har inget att göra med det, logiskt sett, och du skickar hela logiken ("kroppen" av if-satsen) till operatorn ändå.

Vidare saknar din operator för närvarande en korrekt typdeklaration och att skapa egna operatorer kan göras på ett mindre omständligt sätt i vilket fall som helst, t.ex. genom att använda den statiska pipe-funktionen (som vi faktiskt inte ens behöver här).

Slutligen kan vi inte bara skicka referenslistan statiskt om du vill att den ska utvärderas på nytt för varje värde som passerar, så vi behöver istället använda en leverantör.

Så, låt oss fixa allt detta. Mitt förslag till operatören skulle vara detta:

import { OperatorFunction } from 'rxjs';
import { filter } from 'rxjs/operators';

export function filterIfLastElementMatches<T extends Record<any, any>>(
  previousSupplier: () => T[], 
  key: keyof T
): OperatorFunction<T[], T[]> {
  return filter(value => {
    const previous = previousSupplier();
    return !previous.length || !value.length || value[value.length - 1][key] !== previous[previous.length - 1][key]
  });
}

Sedan att använda detta och kombinera det med den villkorliga delen:

source$.pipe(
  // …
  !isReset ? filterIfLastElementMatches(() => this.items, 'eventid') : tap(),
).subscribe(items => { /* … */ });

Se det i aktion här.

Observera att semantiken för villkoret är något annorlunda här eftersom det kommer att utvärderas när den omslutande metoden körs, inte när det emitterade värdet dyker upp. Jag antar att detta inte är kritiskt här.

Svar #2

Observera: Detta är ett andra svar, men denna gång antar vi att du vill jämföra med det tidigare angivna värdet snarare än en extern referenslista. De allmänna punkterna i mitt andra svar gäller fortfarande.

Om detta är den mekanism du behöver, skulle jag starkt rekommendera att använda detta svar istället för det som använder en extern leverantör.

Om värdet att jämföra med bara ska vara det tidigare utsända värdet, kan du helt enkelt använda distinctUntilChanged för detta med en anpassad jämförelsefunktion. Om du insisterar på att slå in det i en anpassad operator, kan det vara så här:

import { OperatorFunction } from 'rxjs';
import { distinctUntilChanged } from 'rxjs/operators';

export function distinctUntilLastElementByKey<T extends Record<any, any>>(
    key: keyof T
): OperatorFunction<T[], T[]> {
    return distinctUntilChanged((previous, value) =>
        previous.length && value.length && value[value.length - 1][key] === previous[previous.length - 1][key]
    );
}

Du kan se det i aktion här.

Dock kan det vara mer logiskt att endast refaktorera jämförelsefunktionen till sin egen funktion och i koden bara använda

source$.pipe(distinctUntilChanged(createLastItemKeyComparator('eventid')))