Issue with the functionality of a custom RxJS operator

I've been struggling to transform a snippet of code into a custom rxjs operator, but I'm facing difficulties making it function correctly.

Here is the current implementation of my custom operator:

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());
};

This custom operator aims to compare the last items in two lists - old and new. If they match, the success callback of the subscribe should not trigger. However, if they do not match, it should fire.

The challenges I am currently facing include:

  • The segment
    <T>(obsv: Observable<T>) => obsv.pipe(ignoreElements())
    is not functioning as expected and does not initiate the success callback.
  • When the condition is set to true, the operator returns a boolean instead of the new list. Consequently, binding the new list to this.items in the subscribe success callback becomes unfeasible.

I am using this operator in the following manner:

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'});
      });
    }
  }
);

The commented-out code within the block is what I aim to refactor, providing clarity on my objective.

How can I overcome these issues?

Answer №1

Update: The solution provided assumes the need to compare against an external reference list instead of just the previous value emitted by the observable. If you are looking to reference the previous value, please refer to my second response.


Before proceeding, let's consider a few key points:

When the condition is met, the operator will return a boolean rather than a new list.

This explains why using mapTo(true) may not be necessary in this case.

The issue with the ignoreElements() function is that it does not trigger the success callback.

To address this, we should focus on returning the observable as-is without ignoring any elements.

In addition, I believe moving the condition inside the operator is unnecessary and can lead to unnecessary complexity. It's also worth noting that defining custom operators can be done more efficiently using the static pipe function.

Lastly, if you want the reference list to be reevaluated for each incoming value, using a supplier function would be more appropriate.


With those considerations in mind, here's my proposed approach for the operator:

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]
  });
}

To implement this operator and incorporate the conditional aspect, you can use the following code snippet:

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

You can see this in action here.

Please note that the logic for the condition may behave slightly differently as it will be evaluated when the method is executed, not necessarily when a value is emitted. This distinction may or may not be relevant depending on your specific use case.

Answer №2

Important Note: This response serves as a follow-up to the previous answer, focusing on comparing values against the previously emitted value rather than an external reference list. The key points outlined in my earlier answer remain relevant.

If this specific comparison mechanism aligns with your requirements, I highly recommend utilizing this approach instead of relying on an external provider.


In cases where you only need to compare against the last emitted value, the implementation can be simplified by using distinctUntilChanged along with a custom comparator function. Should you prefer encapsulating it within a custom operator, you could consider the following:

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]
    );
}

You can observe its functionality here.

Alternatively, a more streamlined approach would involve extracting the comparator function into a separate utility and simply utilizing it as follows within the code:

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

Answer №3

Have you considered this approach? Being a skeleton allows you to tailor the key testing according to your specific requirements.

const filterIfLastEqual = () => <T>(source: Observable<T[]>) => {
  return new Observable(observer => {
    let lastValue: T;

    return source.subscribe({
      next(x) {
        const [nowLast] = x.slice(-1);
        if (lastValue !== nowLast) {
          console.log('diff', lastValue, nowLast)
          observer.next(x);
          lastValue = nowLast;
        } else {
          console.log('skipping', lastValue, nowLast)
        }
      },
      error(err) {
        observer.error(err);
      },
      complete() {
        observer.complete();
      }
    })

  })
}

const emitter = new Subject<any[]>();
emitter.pipe(
  filterIfLastEqual(),
).subscribe(
  v => console.log('received value:', v)
)

// test cases
emitter.next([1, 2, 3, 4]);
emitter.next([1, 2, 3, 4]);
emitter.next([1, 2, 3, 5]);
emitter.next([1, 2, 3, '5']);

You could also implement scan() along with distnctUntilChanged(), utilizing stateful logic for achieving similar results.

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Is it possible to modify the CSS injected by an Angular Directive?

Is there a way to override the CSS generated by an Angular directive? Take, for instance, when we apply the sort directive to the material data table. This can result in issues like altering the layout of the column header. Attempting to override the CSS ...

The performance of linting has significantly decreased following the upgrade of nx/angular, particularly for the plugin import/no-deprecated

The upgrade process of the project involved moving from version 11.2.11 to version 12.2.10 through the nx upgrade process (nx migrate) Following this upgrade, the code linting process now takes around 4 minutes, compared to the previous 30 seconds: time ...

An automatic conversion cannot handle spaces and prohibited characters in Object keys

The AlphaVantage API uses spaces and periods in the keys. Their API documentation is not formal, but you can find it in their demo URL. In my Typescript application, I have created data structures for this purpose (feel free to use them once we solve the ...

Is there a way to access the final child element within a mat-accordion component using Material-UI and Angular 8?

I have a mat-accordion element with multiple expansion panels that are generated dynamically. How can I programmatically select and expand the last mat-expansion-panel element? <mat-accordion> <mat-expansion-panel> text 0 </mat-ex ...

Should compile time errors be triggered by generic function constraints?

Surprisingly, the following code does not throw a compile time error as expected. interface Service { } abstract class TestService implements Service { } class TestServiceImpl extends TestService { } class Blah { } function getService<T extends S ...

A simple way to retrieve data from Firebase using the unique identifier (ID)

Hello, I am attempting to retrieve the user email for each message. Each message (chat) has a user ID associated with it. I fetch the entire user data using getUser on the *ngIf directive and then display the email address. It works fine and there are no ...

Angular service helps in resolving concatenate error by utilizing good practices

I encountered a problem while trying to set a URL with multiple arguments. Here is the code snippet that I attempted, but it did not work as expected: @Injectable() export class MapService { ign : string = 'https://wxs.ign.fr/secret/geoportail/wmt ...

Unlocking the power of NgRx entity: Leveraging updateOne and updateMany with effects

As a newcomer to NgRx entities, I am struggling to understand how to leverage it for updating my state. My preference is to handle the action directly within my effect. Below is the code snippet for my effect: updateItem$ = createEffect(() => this. ...

"Unsuccessful API request leads to empty ngFor loop due to ngIf condition not being

I have been struggling to display the fetched data in my Angular 6 project. I have tried using ngIf and ngFor but nothing seems to work. My goal is to show data from movies on the HTML page, but for some reason, the data appears to be empty. Despite tryin ...

Custom CSS identifier for interactive MuiButton text element

I've been searching for a CSS selector to target this dynamic date element, which is identified as MuiButton-label. While I can currently locate it using xpath in Playwright code, I'm hoping to find an alternative method using a CSS selector. Tha ...

What is the best way to automatically close a sidebar in Angular 8 depending on the window size when the page loads?

Is there a way to ensure that the sidebar remains closed when loading the page on a window with a width of less than 850 pixels? I have attempted implementing the following code, however it only functions properly when I manually resize the window: @ ...

The reason for the error message "Error: Cannot find module '@nx/nx-darwin-arm64'" occurring during an npm install in an Angular project is due to

I recently cloned an Angular Project on my MacBook (M1 Pro Chip) and I tried running npm install but encountered the following error: Error: Cannot find module '@nx/nx-darwin-arm64'. This is the error message I received. npm ERR! code 1 npm ERR! ...

Generating a random number to be input into the angular 2 form group index can be done by following these

One interesting feature of my form is the dynamic input field where users can easily add more fields by simply clicking on a button. These input fields are then linked to the template using ngFor, as shown below: *ngFor="let data of getTasks(myFormdata); ...

Bidirectional data binding in Angular 2 for the class attribute

Utilizing twitter bootstrap tabs, I aim to monitor the application of the active class on the li tag as users switch between different tabs. My objective is to control tab activation through custom buttons by modifying the class attribute to activate direc ...

Using a Typescript typeguard to validate function parameters of type any[]

Is it logical to use this type of typeguard check in a function like the following: Foo(value: any[]) { if (value instanceof Array) { Console.log('having an array') } } Given that the parameter is defined as an array o ...

A missing Array.at() method has been reported in TypeScript array type

When I try: const myArray = [0,4,2]; myArray.at(-1); I encounter the following error related to .at The error message reads: Property 'at' does not exist on type 'number[]'.ts (2339) Why is Array.at() not working in this case? Is th ...

Best practices for structuring TypeScript type declarations for a function module containing numerous private classes

Currently, I am in the process of creating type definitions for a library called fessonia. This task is not new to me, but this particular library has a different organization compared to others I have worked with before, presenting a unique challenge. Th ...

Retrieve all the characteristics accessible of a particular course

I am facing a situation where I have the following class structure: class A { id: number propertyA: string constructor(id: number) { this.id = id } } let a = new A(3) console.log(SomeFunction(a)) // expected output = ['id', ' ...

Access-Control-Allow-Methods is not permitting the use of the DELETE method in the preflight response for Angular 2

I am having trouble deleting a record in mongodb using mongoose. Here is the code snippet from my component: deleteProduct(product){ this._confirmationService.confirm({ message: 'Are you sure you want to delete the item?', ...

Utilizing GraphQL subscriptions to dynamically update objects in response to specific events

My challenge involves updating the users' password that I retrieved previously when an event is published to the PasswordUpdated Channel. Here's my current approach: this.apollo.subscribe({ query: this.passwordUpdatedSubscription }).subscribe( ...