Recursive Promise in javascript
The problem is that the promise you return from getRedirectUrl()
needs to include the entire chain of logic to get to the URL. You're just returning a promise for the very first request. The .then()
you're using in the midst of your function isn't doing anything.
To fix this:
Create a promise that resolves to redirectUrl
for a redirect, or null
otherwise:
function getRedirectsTo(xhr) { if (xhr.status < 400 && xhr.status >= 300) { return xhr.getResponseHeader("Location"); } if (xhr.responseURL && xhr.responseURL != url) { return xhr.responseURL; } return null;}var p = new Promise(function (resolve) { var xhr = new XMLHttpRequest(); xhr.onload = function () { resolve(getRedirectsTo(xhr)); }; xhr.open('HEAD', url, true); xhr.send();});
Use .then()
on that to return the recursive call, or not, as needed:
return p.then(function (redirectsTo) { return redirectsTo ? getRedirectUrl(redirectsTo, redirectCount+ 1) : url;});
Full solution:
function getRedirectsTo(xhr) { if (xhr.status < 400 && xhr.status >= 300) { return xhr.getResponseHeader("Location"); } if (xhr.responseURL && xhr.responseURL != url) { return xhr.responseURL; } return null;}function getRedirectUrl(url, redirectCount) { redirectCount = redirectCount || 0; if (redirectCount > 10) { throw new Error("Redirected too many times."); } return new Promise(function (resolve) { var xhr = new XMLHttpRequest(); xhr.onload = function () { resolve(getRedirectsTo(xhr)); }; xhr.open('HEAD', url, true); xhr.send(); }) .then(function (redirectsTo) { return redirectsTo ? getRedirectUrl(redirectsTo, redirectCount + 1) : url; });}
Here's the simplified solution:
const recursiveCall = (index) => { return new Promise((resolve) => { console.log(index); if (index < 3) { return resolve(recursiveCall(++index)) } else { return resolve() } })}recursiveCall(0).then(() => console.log('done'));
The following has two functions:
- _getRedirectUrl - which is a setTimeout object simulation for looking up a single step lookup of a redirected URL (this is equivalent to a single instance of your XMLHttpRequest HEAD request)
- getRedirectUrl - which is recursive calls Promises to lookup the redirect URL
The secret sauce is the sub Promise whose's successful completion will trigger a call to resolve() from the parent promise.
function _getRedirectUrl( url ) { return new Promise( function (resolve) { const redirectUrl = { "https://mary" : "https://had", "https://had" : "https://a", "https://a" : "https://little", "https://little" : "https://lamb", }[ url ]; setTimeout( resolve, 500, redirectUrl || url ); } );}function getRedirectUrl( url ) { return new Promise( function (resolve) { console.log("* url: ", url ); _getRedirectUrl( url ).then( function (redirectUrl) { // console.log( "* redirectUrl: ", redirectUrl ); if ( url === redirectUrl ) { resolve( url ); return; } getRedirectUrl( redirectUrl ).then( resolve ); } ); } );}function run() { let inputUrl = $( "#inputUrl" ).val(); console.log( "inputUrl: ", inputUrl ); $( "#inputUrl" ).prop( "disabled", true ); $( "#runButton" ).prop( "disabled", true ); $( "#outputLabel" ).text( "" ); getRedirectUrl( inputUrl ) .then( function ( data ) { console.log( "output: ", data); $( "#inputUrl" ).prop( "disabled", false ); $( "#runButton" ).prop( "disabled", false ); $( "#outputLabel").text( data ); } );}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>Input:<select id="inputUrl"> <option value="https://mary">https://mary</option> <option value="https://had">https://had</option> <option value="https://a">https://a</option> <option value="https://little">https://little</option> <option value="https://lamb">https://lamb</option></select>Output:<label id="outputLabel"></label><button id="runButton" onclick="run()">Run</button>
As another illustration of recursive Promises, I used it to solve a maze. The Solve()
function is invoked recursively to advance one step in a solution to a maze, else it backtracks when it encounters a dead end. The setTimeout
function is used to set the animation of the solution to 100ms per frame (i.e. 10hz frame rate).
const MazeWidth = 9const MazeHeight = 9let Maze = [ "# #######", "# # #", "# ### # #", "# # # #", "# # # ###", "# # # #", "# ### # #", "# # #", "####### #"].map(line => line.split(''));const Wall = '#'const Free = ' 'const SomeDude = '*'const StartingPoint = [1, 0]const EndingPoint = [7, 8]function PrintDaMaze(){ //Maze.forEach(line => console.log(line.join(''))) let txt = Maze.reduce((p, c) => p += c.join('') + '\n', '') let html = txt.replace(/[*]/g, c => '<font color=red>*</font>') $('#mazeOutput').html(html)}function Solve(X, Y) { return new Promise( function (resolve) { if ( X < 0 || X >= MazeWidth || Y < 0 || Y >= MazeHeight ) { resolve( false ); return; } if ( Maze[Y][X] !== Free ) { resolve( false ); return; } setTimeout( function () { // Make the move (if it's wrong, we will backtrack later) Maze[Y][X] = SomeDude; PrintDaMaze() // Check if we have reached our goal. if (X == EndingPoint[0] && Y == EndingPoint[1]) { resolve(true); return; } // Recursively search for our goal. Solve(X - 1, Y) .then( function (solved) { if (solved) return Promise.resolve(solved); return Solve(X + 1, Y); } ) .then( function (solved) { if (solved) return Promise.resolve(solved); return Solve(X, Y - 1); } ) .then( function (solved) { if (solved) return Promise.resolve(solved); return Solve(X, Y + 1); } ) .then( function (solved) { if (solved) { resolve(true); return; } // Backtrack setTimeout( function () { Maze[Y][X] = Free; PrintDaMaze() resolve(false); }, 100); } ); }, 100 ); } );}Solve(StartingPoint[0], StartingPoint[1]).then( function (solved) { if (solved) { console.log("Solved!") PrintDaMaze() } else { console.log("Cannot solve. :-(") }} );
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script><pre id="mazeOutput"></pre>