1import { mergeMap, fromValue, fromPromise, pipe } from 'wonka';
2import type { Operation, OperationResult, Exchange } from '../types';
3import type { CombinedError } from '../utils';
4
5/** Options for the `mapExchange` allowing it to react to incoming operations, results, or errors. */
6export interface MapExchangeOpts {
7 /** Accepts a callback for incoming `Operation`s.
8 *
9 * @param operation - An {@link Operation} that the {@link mapExchange} received.
10 * @returns optionally a new {@link Operation} replacing the original.
11 *
12 * @remarks
13 * You may return new {@link Operation | Operations} from this function replacing
14 * the original that the {@link mapExchange} received.
15 * It’s recommended that you use the {@link makeOperation} utility to create a copy
16 * of the original when you do this. (However, this isn’t required)
17 *
18 * Hint: The callback may also be promisified and return a new {@link Operation} asynchronously,
19 * provided you place your {@link mapExchange} after all synchronous {@link Exchange | Exchanges},
20 * like after your `cacheExchange`.
21 */
22 onOperation?(operation: Operation): Promise<Operation> | Operation | void;
23 /** Accepts a callback for incoming `OperationResult`s.
24 *
25 * @param result - An {@link OperationResult} that the {@link mapExchange} received.
26 * @returns optionally a new {@link OperationResult} replacing the original.
27 *
28 * @remarks
29 * This callback may optionally return a new {@link OperationResult} that replaces the original,
30 * which you can use to modify incoming API results.
31 *
32 * Hint: The callback may also be promisified and return a new {@link Operation} asynchronously,
33 * provided you place your {@link mapExchange} after all synchronous {@link Exchange | Exchanges},
34 * like after your `cacheExchange`.
35 */
36 onResult?(
37 result: OperationResult
38 ): Promise<OperationResult> | OperationResult | void;
39 /** Accepts a callback for incoming `CombinedError`s.
40 *
41 * @param error - A {@link CombinedError} that an incoming {@link OperationResult} contained.
42 * @param operation - The {@link Operation} of the incoming {@link OperationResult}.
43 *
44 * @remarks
45 * The callback may also be promisified and return a new {@link Operation} asynchronously,
46 * provided you place your {@link mapExchange} after all synchronous {@link Exchange | Exchanges},
47 * like after your `cacheExchange`.
48 */
49 onError?(error: CombinedError, operation: Operation): void;
50}
51
52/** Creates an `Exchange` mapping over incoming operations, results, and/or errors.
53 *
54 * @param opts - A {@link MapExchangeOpts} configuration object, containing the callbacks the `mapExchange` will use.
55 * @returns the created {@link Exchange}
56 *
57 * @remarks
58 * The `mapExchange` may be used to react to or modify incoming {@link Operation | Operations}
59 * and {@link OperationResult | OperationResults}. Optionally, it can also modify these
60 * asynchronously, when a promise is returned from the callbacks.
61 *
62 * This is useful to, for instance, add an authentication token to a given request, when
63 * the `@urql/exchange-auth` package would be overkill.
64 *
65 * It can also accept an `onError` callback, which can be used to react to incoming
66 * {@link CombinedError | CombinedErrors} on results, and trigger side-effects.
67 *
68 */
69export const mapExchange = ({
70 onOperation,
71 onResult,
72 onError,
73}: MapExchangeOpts): Exchange => {
74 return ({ forward }) =>
75 ops$ => {
76 return pipe(
77 pipe(
78 ops$,
79 mergeMap(operation => {
80 const newOperation =
81 (onOperation && onOperation(operation)) || operation;
82 return 'then' in newOperation
83 ? fromPromise(newOperation)
84 : fromValue(newOperation);
85 })
86 ),
87 forward,
88 mergeMap(result => {
89 if (onError && result.error) onError(result.error, result.operation);
90 const newResult = (onResult && onResult(result)) || result;
91 return 'then' in newResult
92 ? fromPromise(newResult)
93 : fromValue(newResult);
94 })
95 );
96 };
97};