'How to cancel an entire redux saga chain from the leaf node

We have multiple sagas (ex: loadSampleData and loadCustomSampleData) that can be triggered by multiple different actions and can have multiple in-flight requests depending on the payload (different results for different parameters). These sagas call the same API but for different use-cases.

We keep track of these in-flight requests (and their corresponding promises) so we can return an existing promise for a matching set of parameters, and/or cancel/abort a request if it is no longer needed.

This logic exists in an AbstractDataCacheManger which is responsible for keeping track of what requests are in progress vs. completed and what data has been returned / cached for what parameters. This base class is extended as singleton instances for specific types of data / apis (ex: SampleDataCacheManager)

The issue we are running into is that when we call controller.abort() from within the cache managers .get() method , we trigger an AbortError that bubbles up to the calling saga. This is not ideal for several reasons:

  1. This base cache manager is used by multiple derived classes in many places and having to write abort specific error handling logic for each one is a lot of overhead that we would prefer to consolidate within the base cache manager class.

  2. There is clean-up logic inside the calling saga's finally block (and possible parent blocks) that could modify state that we don't want changed (ex: setting isLoadingSampleData flag to false) as we may have other in-flight requests that are still connected.

  3. If I have nested multiple sagas, I don't want to have to check for cancellation at every level and/or execute any unnecessary logic for a thread that is no longer valid and should just be discarded.

Ultimately I'm trying to find out if there's a way to kill / cancel an entire saga chain (similar to how takeLatest works top-down) but from inside a child saga or our from within our AbstractDataCacheManager.get() function...

NOTE: these examples have been greatly simplified for clarity...

// App/saga.ts
export function* loadSampleData(action: LoadSampleDataAction): SagaIterator {
  const controller = new AbortController();
  try {
    const { params } = action;
    yield put(updateIsLoadingSampleData(true));
    const response = yield call(sampleDataCacheManager.get, params, controller)) // <-- if current effect is blocked waiting here for .get() to resolve and the cache manager calls .abort() on its controller, I want to immediately terminate this saga and any parents
  } catch (e) {
    yield put(updateSampleData([]));
    yield put(updateErrors(e));
  } finally {
    yield put(updateIsLoadingSampleData(false));
    if (yield cancelled()) {
      abortController.abort(); // aborts request if the saga was cancelled via takeLatest
    }
  }
}

// cacheManagers/AbstractDataCacheManager.ts
export abstract class AbstractDataCacheManager<...> {
  async get(params, controller): Promise<T> {
    const key = this.getKey(params);

    const cachedEntry = this.cache.getCachedEntry(key);
    if (cachedEntry) {
      return Promise.resolve(cachedEntry.data);
    }

    let request = this.requestsMap.get(key);
    if (request) {
      if (this.comparator(request.params, params)) {
        return request.promise;
      } else {
        request.controller.abort(); // <-- how can I cancel any saga waiting on this request's promise from here...
      }
    }

    // creates a new entry consisting of params, promise, key and controller
     request = this.createNewRequest(key, params, controller);

     this.requestsMap.add(key, request);
     request.finally(() => { this.requestsMap.delete(key); } );

     return request.promise;
  }
}

Is this possible without having to write a bunch of error checking logic through the code base?



Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source