philolegein
philolegein

Reputation: 1515

Map array of args to array of promises to be executed later

I'm building up a hierarchy of scripts, where all the scripts at the same level can be run in parallel, but each level needs to complete before the next level runs.

I had this working when I had a fixed, known number of levels, by querying the document for the scripts at level 1, having a function that returns a promise when the arg passed loads, pushing that function onto an array, repeating for each level, and then creating a Promise.all chain for the resulting arrays:

function loadScript (script) {
  return new Promise(function(resolve, reject) {
    script.setAttribute("src", script.getAttribute("data-src"));
    script.onload = function() {
      resolve(script.src);
    };
    script.onerror = function() {
      reject(script.src);
    }
  });
}

function loadScripts() {
  const firstScripts = document.querySelectorAll( "script[data-type='lazy']"
    + "[data-order='1']" );
  const secondScripts = document.querySelectorAll( "script[data-type='lazy']"
    + "[data-order='2']" );

  let first = [];
  let second = [];

  firstScripts.forEach(function(element) {
    first.push(loadScript(element));
  });
  secondScripts.forEach(function(element) {
    second.push(loadScript(element));
  });

  Promise.all(first)
    .then(Promise.all(second))
     .then(function() {
       console.log('all scripts loaded');
     })
     .catch(function(script) {
       console.log(script + ' failed to load.');
     });
 }

I'm now trying to abstract it a bit, so it will work with an arbitrary, unknown depth. This seems like a fine place to use Array.map; however, the problem is, it's actually executing the Promise.

So, what I'm looking for is a way to map the calls to loadScript such that they can be called later:

function loadScripts() {
  let maxDepth = 0;
  let hierarchy = [];

  while ( Boolean(document.querySelector( "script[data-type='lazy']"
    + "[data-order='" + maxDepth + "']" )) ) {

    hierarchy[ maxDepth ] = [ ...document.querySelectorAll(
      "script[data-type='lazy'][data-order='" + maxDepth + "']" ) ];

    maxDepth++;
  }

  hierarchy.forEach((scripts, level) => {
    // these loadScript calls should ... not be calls. They should be queued to
    // run later when walkHierarchy is called
    hierarchy[ level ] = scripts.map((element) => loadScript( element ) );
  });

  walkHierarchy = async function (arr) {
    for (const level of arr) await Promise.all(level);
  }

  walkHierarchy( hierarchy )
    .then(function() {
      console.log('all scripts loaded');
    })
    .catch(function(script) {
      console.log(script + ' failed to load.');
    });
}

Alternatively, if there's a way to take the walkHierarchy logic and include it in the hierarchy.forEach, such that each level completed before moving on to the next, that would be fine, too.

Upvotes: 1

Views: 86

Answers (1)

Mulan
Mulan

Reputation: 135217

dynamic import

Maybe you are looking for dynamic import which already supports on-demand loading.

DIY level-order script loader

Maybe you don't want to use import or it's not supported in your environment. Let's take a concrete example so we can verify the behaviour. We start with a bunch of unordered <scripts> -

<script data-type="lazy" data-src="a.js" data-order="1"></script>
<script data-type="lazy" data-src="b.js" data-order="1"></script>
<script data-type="lazy" data-src="bbbb.js" data-order="4"></script>
<script data-type="lazy" data-src="aa.js" data-order="2"></script>
<script data-type="lazy" data-src="bb.js" data-order="2"></script>
<script data-type="lazy" data-src="aaa.js" data-order="3"></script>
<script data-type="lazy" data-src="c.js" data-order="1"></script>
<script data-type="lazy" data-src="cc.js" data-order="2"></script>
<script data-type="lazy" data-src="d.js" data-order="1"></script>
<script data-type="lazy" data-src="bbb.js" data-order="3"></script>
<script data-type="lazy" data-src="aaaa.js" data-order="4"></script>

graph

The first step is to make a graph of (order, script array) map -

// script : html_script_element
// order : int
// graph : (order, script array) map
// makeGraph : script iterable -> graph
function makeGraph(scripts) {
  const g = new Map
  for (const s of scripts) {
    const order = Number(s.getAttribute("data-order"))
    if (!g.has(order)) g.set(order, [])
    g.get(order).push(s)
  }
  return g
}
makeGraph(document.querySelectorAll("script[data-type=lazy]"))
Map {
  1 => [<script>, <script>, <script>, <script>],
  4 => [<script>, <script>],
  2 => [<script>, <script>, <script>],
  3 => [<script>, <script>]
}

loadScripts

It doesn't matter that the graph appears unordered. loadScripts will recursively traverse the graph, incrementing order one-by-one. When a particular order is not found in the graph, we know it is time to stop loading and exit -

// loadScripts : (graph, int) -> (string array) promise
async function loadScripts(g, order = 1) {
  if (!g.has(order))
    return []
  else
    return [
      ...await Promise.all(g.get(order).map(loadScript)),
      ...await loadScripts(g, order + 1)
    ]
}

loadScript

We can demo this here by modifying loadScript to resolve whether the script loads successfully or not. We also added a console.log so we can see which order loadScript is called in -

// loadScript : script -> string promise
function loadScript(script) {
  return new Promise(function(resolve, reject) {
    console.log("loading", script.getAttribute("data-src")) // ⚠️ for demo
    script.setAttribute("src", script.getAttribute("data-src"))
    script.onload = _ => resolve(script.src)
    script.onerror = _ => resolve(script.src) // ⚠️ resolve for demo only
  })
}

all together now

loadScripts(makeGraph(document.querySelectorAll("script[data-type=lazy]")))
  .then(console.log)
  .catch(console.error)
loading a.js
loading b.js
loading c.js
loading d.js
loading aa.js
loading bb.js
loading cc.js
loading aaa.js
loading bbb.js
loading bbbb.js
loading aaaa.js
[
  "https://stacksnippets.net/a.js",
  "https://stacksnippets.net/b.js",
  "https://stacksnippets.net/c.js",
  "https://stacksnippets.net/d.js",
  "https://stacksnippets.net/aa.js",
  "https://stacksnippets.net/bb.js",
  "https://stacksnippets.net/cc.js",
  "https://stacksnippets.net/aaa.js",
  "https://stacksnippets.net/bbb.js",
  "https://stacksnippets.net/bbbb.js",
  "https://stacksnippets.net/aaaa.js"
]

demo

Verify the results in your own browser -

function loadScript(script) {
  return new Promise(function(resolve, reject) {
    console.log("loading", script.getAttribute("data-src")) // ⚠️ demo only
    script.setAttribute("src", script.getAttribute("data-src"))
    script.onload = _ => resolve(script.src)
    script.onerror = _ => resolve(script.src) // ⚠️ demo only
  })
}

async function loadScripts(g, order = 1) {
  if (!g.has(order))
    return []
  else
    return [
      ...await Promise.all(g.get(order).map(loadScript)),
      ...await loadScripts(g, order + 1)
    ]
}

function makeGraph(scripts) {
  const g = new Map
  for (const s of scripts) {
    const order = Number(s.getAttribute("data-order"))
    if (!g.has(order)) g.set(order, [])
    g.get(order).push(s)
  }
  return g
}

loadScripts(makeGraph(document.querySelectorAll("script[data-type=lazy]")))
  .then(console.log)
  .catch(console.error)
.as-console-wrapper { min-height: 100%; top: 0; }
<script data-type="lazy" data-src="a.js" data-order="1"></script>
<script data-type="lazy" data-src="b.js" data-order="1"></script>
<script data-type="lazy" data-src="bbbb.js" data-order="4"></script>
<script data-type="lazy" data-src="aa.js" data-order="2"></script>
<script data-type="lazy" data-src="bb.js" data-order="2"></script>
<script data-type="lazy" data-src="aaa.js" data-order="3"></script>
<script data-type="lazy" data-src="c.js" data-order="1"></script>
<script data-type="lazy" data-src="cc.js" data-order="2"></script>
<script data-type="lazy" data-src="d.js" data-order="1"></script>
<script data-type="lazy" data-src="bbb.js" data-order="3"></script>
<script data-type="lazy" data-src="aaaa.js" data-order="4"></script>

Upvotes: 2

Related Questions