Skip to content

Commit 59b790b

Browse files
committed
feat: migrate and update api retry fns to use dynamic import of ES module pretry
1 parent 23e34b1 commit 59b790b

File tree

4 files changed

+134
-0
lines changed

4 files changed

+134
-0
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"title": "Configure retries for parallel API requests",
3+
"type": "server"
4+
}
5+
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
const axios = require('axios');
2+
const qs = require('qs');
3+
4+
// The root URL for an API which is known to fail on occasion
5+
const unstableSpaceUri = 'https://unstable-5604.twil.io';
6+
// We'll declare these functions outside of the handler since they have no
7+
// dependencies on other values, and this will tidy up our pRetry calls.
8+
const astronautRequest = () => axios.get(`${unstableSpaceUri}/astros`);
9+
const issRequest = () => axios.get(`${unstableSpaceUri}/iss`);
10+
// Use a common object for retry configuration to DRY up our code :)
11+
const retryConfig = (reqName) => ({
12+
retries: 3,
13+
onFailedAttempt: () => console.log(`Retrying ${reqName}...`),
14+
});
15+
16+
exports.handler = async (context, event, callback) => {
17+
// We need to asynchronously import p-retry since it is an ESM module
18+
const { default: pRetry } = await import('p-retry');
19+
// Create a new voice response object
20+
const twiml = new Twilio.twiml.VoiceResponse();
21+
22+
try {
23+
// Create a promise with retry for each API call that can be made
24+
// independently of each other
25+
const getAstronauts = pRetry(astronautRequest, retryConfig('astros'));
26+
const getIss = pRetry(issRequest, retryConfig('iss'));
27+
// pRetry returns a promise, so we can still use Promise.all to await
28+
// the result of both requests in parallel with retry and backoff enabled!
29+
const [astronauts, iss] = await Promise.all([getAstronauts, getIss]);
30+
31+
const { number, people } = astronauts.data;
32+
const { latitude, longitude } = iss.data.iss_position;
33+
34+
const names = people.map((astronaut) => astronaut.name).sort();
35+
36+
// We can use reverse geocoding to convert the latitude and longitude
37+
// of the ISS to a human-readable location. We'll use positionstack.com
38+
// since they provide a free API.
39+
// Be sure to set your positionstack API key as an environment variable!
40+
const { data: issLocation } = await pRetry(
41+
() =>
42+
axios.get(
43+
`http://api.positionstack.com/v1/reverse?${qs.stringify({
44+
access_key: context.POSITIONSTACK_API_KEY,
45+
query: `${latitude},${longitude}`,
46+
})}`
47+
),
48+
retryConfig('iss location')
49+
);
50+
51+
const { label } = issLocation.data[0] || 'an unknown location';
52+
53+
// Create a list formatter to join the names with commas and 'and'
54+
// so that the played speech sounds more natural
55+
const listFormatter = new Intl.ListFormat('en');
56+
57+
twiml.say(`There are ${number} people in space.`);
58+
twiml.pause({ length: 1 });
59+
twiml.say(`Their names are: ${listFormatter.format(names)}`);
60+
twiml.pause({ length: 1 });
61+
twiml.say(
62+
`Also, the International Space Station is currently above ${label}`
63+
);
64+
// Return the final TwiML as the second argument to `callback`
65+
// This will render the response as XML in reply to the webhook request
66+
// and result in the message being played back to the user
67+
return callback(null, twiml);
68+
} catch (error) {
69+
// In the event of an error, return a 500 error and the error message
70+
console.error(error);
71+
return callback(error);
72+
}
73+
};
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"title": "Configure retries for an API request",
3+
"type": "server"
4+
}
5+
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
const axios = require('axios');
2+
3+
const getAstronauts = () => axios.get('https://unstable-5604.twil.io/astros');
4+
5+
exports.handler = async (context, event, callback) => {
6+
// We need to asynchronously import p-retry since it is an ESM module
7+
const { default: pRetry } = await import('p-retry');
8+
// Create a new voice response object
9+
const twiml = new Twilio.twiml.VoiceResponse();
10+
11+
try {
12+
let attempts = 1;
13+
// Open APIs From Space: http://open-notify.org
14+
// Number of people in space
15+
const response = await pRetry(getAstronauts, {
16+
retries: 3,
17+
onFailedAttempt: ({ attemptNumber, retriesLeft }) => {
18+
attempts = attemptNumber;
19+
console.log(
20+
`Attempt ${attemptNumber} failed. There are ${retriesLeft} retries left.`
21+
);
22+
// 1st request => "Attempt 1 failed. There are 3 retries left."
23+
// 2nd request => "Attempt 2 failed. There are 2 retries left."
24+
// …
25+
},
26+
});
27+
const { number, people } = response.data;
28+
29+
const names = people.map((astronaut) => astronaut.name).sort();
30+
// Create a list formatter to join the names with commas and 'and'
31+
// so that the played speech sounds more natural
32+
const listFormatter = new Intl.ListFormat('en');
33+
34+
twiml.say(`There are ${number} people in space.`);
35+
twiml.pause({ length: 1 });
36+
twiml.say(`Their names are: ${listFormatter.format(names)}`);
37+
// If retries were necessary, add that information to the response
38+
if (attempts > 1) {
39+
twiml.pause({ length: 1 });
40+
twiml.say(`It took ${attempts} attempts to retrieve this information.`);
41+
}
42+
// Return the final TwiML as the second argument to `callback`
43+
// This will render the response as XML in reply to the webhook request
44+
// and result in the message being played back to the user
45+
return callback(null, twiml);
46+
} catch (error) {
47+
// In the event of an error, return a 500 error and the error message
48+
console.error(error);
49+
return callback(error);
50+
}
51+
};

0 commit comments

Comments
 (0)