-
Notifications
You must be signed in to change notification settings - Fork 23
/
timeoutLink.ts
106 lines (86 loc) · 3.61 KB
/
timeoutLink.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
import { ApolloLink, Observable, Operation, NextLink } from '@apollo/client/core';
import { DefinitionNode } from 'graphql';
import TimeoutError from './TimeoutError';
const DEFAULT_TIMEOUT: number = 15000;
/**
* Aborts the request if the timeout expires before the response is received.
*/
export default class TimeoutLink extends ApolloLink {
private timeout: number;
private statusCode?: number;
constructor(timeout: number, statusCode?: number) {
super();
this.timeout = timeout || DEFAULT_TIMEOUT;
this.statusCode = statusCode;
}
public request(operation: Operation, forward: NextLink) {
let controller: AbortController;
// override timeout from query context
const requestTimeout = operation.getContext().timeout || this.timeout;
// add abort controller and signal object to fetchOptions if they don't already exist
if (typeof AbortController !== 'undefined') {
const context = operation.getContext();
let fetchOptions = context.fetchOptions || {};
controller = fetchOptions.controller || new AbortController();
fetchOptions = { ...fetchOptions, controller, signal: controller.signal };
operation.setContext({ fetchOptions });
}
const chainObservable = forward(operation); // observable for remaining link chain
const operationType = (operation.query.definitions as any).find(
(def: DefinitionNode) => def.kind === 'OperationDefinition'
).operation;
if (requestTimeout <= 0 || operationType === 'subscription') {
return chainObservable; // skip this link if timeout is zero or it's a subscription request
}
// create local observable with timeout functionality (unsubscibe from chain observable and
// return an error if the timeout expires before chain observable resolves)
const localObservable = new Observable(observer => {
let timer: any;
// listen to chainObservable for result and pass to localObservable if received before timeout
const subscription = chainObservable.subscribe(
result => {
clearTimeout(timer);
observer.next(result);
observer.complete();
},
error => {
clearTimeout(timer);
observer.error(error);
observer.complete();
}
);
// if timeout expires before observable completes, abort call, unsubscribe, and return error
timer = setTimeout(() => {
if (controller) {
controller.abort(); // abort fetch operation
// if the AbortController in the operation context is one we created,
// it's now "used up", so we need to remove it to avoid blocking any
// future retry of the operation.
const context = operation.getContext();
let fetchOptions = context.fetchOptions || {};
if(fetchOptions.controller === controller && fetchOptions.signal === controller.signal) {
fetchOptions = { ...fetchOptions, controller: null, signal: null };
operation.setContext({ fetchOptions });
}
}
observer.error(new TimeoutError('Timeout exceeded', requestTimeout, this.statusCode));
subscription.unsubscribe();
}, requestTimeout);
let ctxRef = operation.getContext().timeoutRef;
if (ctxRef) {
ctxRef({
unsubscribe: () => {
clearTimeout(timer);
subscription.unsubscribe();
}
});
}
// this function is called when a client unsubscribes from localObservable
return () => {
clearTimeout(timer);
subscription.unsubscribe();
};
});
return localObservable;
}
}