How to silently cancel a duplicate operation?

Hi,

I’m looking to the right way to cancel silently a duplicate operation. I tried to do that with an apollo link but when I test it, the client seems never resolve the duplicate call.

Below, is the apollo link that I created. I’m open to any suggestion.

import { ApolloLink, Observable } from '@apollo/client/core';
import type { FetchResult, NextLink, Operation } from '@apollo/client/core';
import type { Observer } from 'zen-observable-ts';

// DeduplicateLink is an ApolloLink that prevents duplicate operations from being executed
// a duplicate operation is an operation with the same operationName it doesn't care of the variables
class DeduplicateLink extends ApolloLink {
  private logger: any;

  private opQueue: Set<string> = new Set();

  constructor({ logger }) {
    super();
    this.logger = logger;
  }

  public request(operation: Operation, forward: NextLink) {
    const { deduplicate } = operation.getContext();
    const { operationName } = operation;
    if (!deduplicate || !operationName) {
      return forward(operation);
    }

    return new Observable<FetchResult>((observer: Observer<FetchResult>) => {
      if (!this.isDuplicateOp(operationName)) {
        this.opQueue.add(operationName);
  
        const subscription = forward(operation).subscribe({
          next: (data) => {
            observer.next(data);
          },
          error: (error) => {
            observer.error(error);
          },
          complete: () => {
            observer.complete();
          }
        });

        return () => {
          if (this.opQueue.has(operationName)) {
            this.opQueue.delete(operationName);
          }
          subscription.unsubscribe();
        };
      }
      this.logger.warn({
        message: `Duplicate call of ${operationName}`,
      });
      // don't execute the duplicate operation
      observer.complete();
    });
  }

  private isDuplicateOp = (operationName: string): boolean => this.opQueue.has(operationName);
}

export default DeduplicateLink;

Thanks in advance

You cannot do that silently, just like you couldn’t silently cancel a promise - something has to happen, or everything will be waiting for it forever (or cause crashes down the line because it would break expectations that you have writing userland code).

Either the promise resolves with a value or it rejects with an error. Do neither of those, and it hangs forever.

That request was started with the promise of a value - if you just complete it without a value, what would happen at call site?
You’d get a “success”, but no data - this would cause all kinds of crashes down the line.

Taking a few steps back, this might be a bit of an X-Y-problem: you are doing this to solve a certain problem X and ask how to do Y because you’ve come to the conclusion that that’s what would solve X.
There might be another solution to X - could you explain what your actual use case is in a bit more detail?

You are right. There might be another solution to X but we didn’t find the root cause of X yet. According to our logs, it seems that some mutations are sent twice in production. This solution is a patch until we can repro and fix the root cause.
To fix that solution, I just need to return the same data and let the cache doing it’s work. It should not re-render the UI due to data thanks to the cache but maybe the load status will bring some trouble.

Especially with mutations, I would be very cautious with a solution like this - mutations are meant to trigger a change on the server, so while right now this might be a stopgap, code like this is likely to stay in your codebase for a long time, and one day you will introduce a mutation that runs twice in short succession, e.g. if a user changes two rows in a table in quick succession.

This would turn out to be almost impossible to debug (or even notice), but some user interactions would just get lost.

That’s also why Apollo Client doesn’t deduplicate mutations - sending the “getAccountBalance” query twice can be deduplicated, but omitting a second trigger of the “sendMoneyToUser” mutation could cause serious problems.

=> I’d really recommend not to go down this road. If this is just one mutation being triggered multiple time, I’d add a temporary stopgap in the component that can trigger the mutation instead.

I’m not enjoying that too and I hope that it will be not a long temporary fix. But we need to find the root cause and a bit of redesign. For now, we will use that only where it’s safe.
Thank you for your advises.