@equinor/fusion-query
Version:
122 lines • 6.38 kB
JavaScript
import { actions } from './actions';
import { filter, mergeMap, from, map, catchError, of, takeUntil, withLatestFrom, timer, tap, } from 'rxjs';
import { QueryClientError } from './QueryClientError';
/**
* Handles incoming request actions by transforming them into execute actions.
*
* @param action$ - The stream of actions being dispatched in the system.
* @returns An Observable that emits execute actions for each request action.
*/
export const handleRequests = (action$) => action$.pipe(filter(actions.request.match), map((action) => actions.execute(action.meta.transaction)));
/**
* Handles the execution of a query.
*
* @param fetch - The function used to execute the query.
* @returns A flow that handles the execution of the query.
*/
export const handleExecution = (fetch) => (action$, state$) => action$.pipe(filter(actions.execute.match), withLatestFrom(state$), mergeMap(([action, state]) => {
const transaction = action.payload;
const request = state[transaction];
// Create an AbortController instance to manage cancellation.
const controller = new AbortController();
// Listen for cancel actions specifically targeting this transaction.
const cancel$ = action$.pipe(filter(actions.cancel.match), filter((next) => next.payload.transaction === transaction), tap(() => {
// If the request hasn't been aborted yet, abort it.
if (!controller.signal.aborted) {
controller.abort();
}
}));
try {
return from(fetch(request.args, controller.signal)).pipe(map((value) => actions.execute.success({
...request,
status: 'complete',
completed: Date.now(),
value,
})), catchError((err) => of(actions.execute.failure(err, transaction))), takeUntil(cancel$));
}
catch (err) {
// Normally errors thrown during the execution of the fetch function are caught by the `catchError` operator.
// However, if the fetch function itself throws an error, it will be caught here.
// This can happen if the fetch function is not a function or if it throws synchronously.
return of(actions.execute.failure(err, transaction));
}
}));
/**
* Handles execution failure by scheduling retries according to the provided retry options.
* If the maximum number of retries is exceeded or no retry options are provided, it emits an error action.
* Otherwise, it schedules a retry after a delay determined by the retry options.
*
* @param config - Optional configuration for retry behavior, including the maximum number of retries and delay between retries.
*/
export const handleFailure = (config) => {
return (action$, state$) => {
return action$.pipe(
// Filter for actions that indicate an execution failure.
filter(actions.execute.failure.match),
// Combine each failure action with the latest state.
withLatestFrom(state$),
// Handle the retry logic for each failure.
mergeMap(([action, state]) => {
// Extract the transaction identifier from the action metadata.
const { transaction } = action.meta;
// Retrieve the corresponding request from the state.
const request = state[transaction];
// If the request no longer exists in the state, it cannot be retried.
if (!request) {
return of(actions.error(transaction, new QueryClientError('error', {
message: 'request not found, cannot retry request, most likely removed while scheduled for retry!',
})));
}
// Count the number of execution attempts made for this request.
const executions = request.execution.length;
// Merge the provided configuration with the request-specific retry options.
const retryOptions = Object.assign({}, config, request.options?.retry);
// Identify the cause of the failure, either the last known error or a generic message.
const cause = request.errors?.slice(-1)[0] ??
new QueryClientError('error', {
message: 'no errors registered for request!',
request,
});
// If the maximum number of retries has been reached, report an error.
if (executions > retryOptions.count) {
const error =
// if retry is disabled return the last error, else return all registered errors
retryOptions.count === 0
? cause
: new QueryClientError('error', {
message: 'maximum retries executed!',
cause: request.errors,
request,
});
return of(actions.error(transaction, error));
}
// Create an Observable that emits after the specified delay for the retry.
const delay$ = typeof retryOptions.delay === 'function'
? from(retryOptions.delay(cause)).pipe(
// Catch errors that occur within the delay calculation.
catchError((cause) => of(actions.error(transaction, new QueryClientError('error', {
message: 'retry delay callback failed!',
cause: [...(request.errors ?? []), cause],
request,
})))))
: timer(retryOptions.delay);
// After the delay, check if the request is still present and initiate a retry.
return delay$.pipe(withLatestFrom(state$),
// Ensure the request has not been removed from the state during the delay.
filter(([_, state]) => !!state[transaction]),
// If the request has been removed, throw an error.
tap(([, state]) => {
if (!state[transaction]) {
throw new QueryClientError('error', {
message: 'request not found, most likely removed while scheduled for retry',
cause: request.errors,
request,
});
}
}),
// Emit an action to re-execute the request.
map(() => actions.execute(transaction)));
}));
};
};
//# sourceMappingURL=flows.js.map