How should I check if every dynamic import is complete?

Hello,

I work on a large app where, depending on a user role, I load/import different modules sets. here is a simplified example of the load of the modules:

	const userRoles = Roles.getRolesForUser(Meteor.userId())
	for (let role of userRoles) {
		switch (role) {
		case "user":
				import("/imports/user/")
			break
		case "moderator":
				import("/imports/moderator/")
			break
		case "admin":
				import("/imports/admin/")
			break
		default:
		}
	}

Each module comes with its own routes, translation files, api & UI. That’s why I need to check that every module is loaded before I display the main UI & navigation (or else, e.g. the navigation items labels related to an unloaded module will not be translated, or the routes will return a 404) .

Is there a pattern, as simple as possible, to ensure that everything is loaded?

I was going for a promise for each import that updates an array, watch this array and load the app only when all values of the array are true. But it seems over complicated, so here I am asking :slight_smile:

Here is an example of the watched array structure:

let myVerificationArray = [
		{ user: true },
		{ moderator: false },
		{ admin: false },
	]

Note that I’m using vue, vue router, vue-i18n but not vuex.

Thanks!

Easiest is to push them into an array and wait for it to finish with Promise.all

Or if it needs to keep track of stuff across module boundaries, use a utility module for loading:

export class Loader {
  constructor() {
    this.promiseArr = [];
    this.promise = new Promise((resolve, reject) => {
      this._resolveSelf = resolve;
      this._rejectSelf = reject;
    });
    this._numAdded = 0;
    this._numSettled = 0;
  }
  _onFulfilled() {
    this._numSettled++;
    if (this._numAdded === this._numSettled) {
      // Wrap in Promise.all to get the arguments out
      this._resolveSelf(Promise.all(this.promiseArr));
    }
  }
  _onRejected(reason) {
    this._rejectSelf(reason);
  }
  add(...promises) {
    promises.forEach(p => {
      this._numAdded++;
      this.promiseArr.push(p.then(this._onFulfilled, this._onRejected));
    });
  }
  ready() {
    return this.promise;
  }
  then(onfulfilled, onrejected) {
    return this.promise.then(onfulfilled, onrejected);
  }
  catch(onrejected) {
    return this.promise.catch(onrejected);
  }
}

export default new Loader();
import Loader from '/utils/loader'
...
    const userRoles = Roles.getRolesForUser(Meteor.userId());
    for (let role of userRoles) {
        switch (role) {
            case "user":
            Loader.add(import("/imports/user/"));
            break;
            case "moderator":
            Loader.add(import("/imports/moderator/"));
            break;
            case "admin":
            Loader.add(import("/imports/admin/"));
            break;
            default:
        }
    }
...
   await Loader.ready()
   // render stuff
3 Likes

Hi @coagmano,

Thank you for this great answer.

It turns out that my use case is more complex than what I can achieve with Promise.all. Here is a more detailed description of my issue with the actual code & logic. I tried to make nested promises with a combination of Promises.all and then(). Sorry for the long post, by the way :blush:

To sum it up, the order is:

  • load the base bundle
  • login the client
  • import i18n language file for the main bundle then for each module
  • for each module, after its language file is loaded and merged in the related i18n messages, I need to load the module itself (localized routes, UI …)

The main loading part

Accounts.onLogin(function (user) {
	let userRoles = Roles.getRolesForUser(Meteor.userId())
	let promises = []
	let lang = getDefaultLanguage()
	promises.push(loadLanguageAsync(lang))
	this.modulesReady = false
	for (let role of userRoles) {
		switch (role) {
		case "user":
			import { loadUserLanguageAsync } from "/imports/user/data/i18n"
			promises.push(loadUserLanguageAsync(lang).then(import("/imports/user/")))
			break
		case "admin":
			import { loadAdminLanguageAsync } from "/imports/admin/data/i18n"
			promises.push(loadAdminLanguageAsync(lang).then(import("/imports/admin/")))
			break
		default:
			break
		}
	}

	return Promise.all(promises).then(function (values) {
		this.modulesReady = true // my green flag, attached to the window object
	})
})

the main language loading functions

const loadedLanguages = []

// Load i18n
Vue.use(VueI18n)
export const i18n = new VueI18n()


export const getDefaultLanguage = () => {
	let storedLanguage = window.localStorage.getItem(
		Meteor.settings.public.brand + "_lang"
	)
	return Meteor.user() && Meteor.user().settings && Meteor.user().settings.language
		? Meteor.user().settings.language
		: // condition 2: if not, rely on a previously selected language
		storedLanguage
			? storedLanguage
			: // or simply the browser default lang
			navigator.language.substring(0, 2)
}
export const loadLanguage = (lang, langFile) => {
	console.log("LOAD LANGUAGE " + lang)

	// we store agnostically the last selected language as default, if no user is logged in.
	window.localStorage.setItem(
		Meteor.settings.public.brand + "_lang",
		lang
	)
	loadedLanguages.push(lang)
	if (langFile) {
		i18n.setLocaleMessage(lang, Object.assign(langFile))
	}
	i18n.locale = lang


	return lang
}

export const loadLanguageModule = (lang, langFile) => {
	console.log("LOAD LANGUAGE MODULE" + lang)
	i18n.mergeLocaleMessage(lang, Object.assign(langFile))
	return lang
}

export function loadLanguageAsync(lang) {
	if (i18n.locale !== lang) {
		if (!loadedLanguages.includes(lang)) {
			switch (lang) {
			
			case "en":
				return import("./lang/en.json").then(langFile => loadLanguage("en", langFile))
			
			case "fr":
				return import("./lang/fr.json").then(langFile => loadLanguage("fr", langFile))
			
			default:
				return import("./lang/fr.json").then(langFile => loadLanguage("fr", langFile))
			}
		} else {
			console.log("Already loaded " + lang)

		}
		return Promise.resolve(!loadedLanguages.includes(lang) || loadLanguage(lang))
	}
	return Promise.resolve(lang)
}

The user module language loading

export default function loadUserLanguageAsync(lang) {
	if (i18n.locale !== lang || !userLoadedLanguages.includes(lang)) {
		switch (lang) {
		case "en":
			return import("./lang/en.json").then(langFile => loadLanguageModule("en", langFile))
		case "fr":
			return import("./lang/fr.json").then(langFile => loadLanguageModule("fr", langFile))
		default:
			return import("./lang/fr.json").then(langFile => loadLanguageModule("fr", langFile))
		}
	}
	return Promise.resolve(i18n.messages[lang].user).then(console.log("USER LANG LOADED"))
}
  • Once every module is loaded, I switch a flag that allows my router navigation guard to proceed to the route required (see the main loading part).

The router guard and await async function

router.beforeEach((to, from, next) => {
     isReady().then(
		console.log("NEXT"),
		next()
	)
})
async function isReady() {
	while (true) {
		if (this.modulesReady) { console.log("READY"); return }
		await null // prevents app from hanging
	}
}

I’m quite new to the async logic and I struggle to identify what I am doing wrong, probably many things. The code here makes the browser crash since I guess my promises values are not the right ones and it goes in an infinite isReady() loop.

I would very much welcome suggestions or advises on the better/correct way to go. And again, sorry for the long post :smile:

Hi,

I struggled with the same thing. I solved my problem by using 3 (number of collections I was waiting for) reactive template variables. In each subscription I set them to true if the collection is ready and false if I start updating a document in these collections.
Then I have a autorun function, that is dependend on these 3 variables. (So it will run when any of the 3 change.) In this autorun I simply check if all 3 are true, and if they are, I continue with the function.

Makes sense? :slight_smile:

regards,

Paul

@pwiegers thanks for your answer. In my case, it would not work I guess, since the imports must be ordered. Also, I’m diving into async functions and ES6 syntax and I would like to achieve it this way.

By the way, why didn’t you use subscriptionsReady directly in the autorun instead of ReactiveVars? With vue, it looks like v-if="$subReady.subName". & with blaze, it’s detailed here.

@pwiegers thanks for yo

@pwiegers thanks for your answer. In my case, it would not work I guess, since the imports must be ordered. Also, I’m diving into async functions and ES6 syntax and I would like to achieve it this way.

Fair enough :slight_smile:

By the way, why didn’t you use subscriptionsReady directly in the autorun instead of ReactiveVars? With vue, it looks like v-if="$subReady.subName" . & with blaze, it’s detailed here.

I did not know at the time of subscriptionsReady :wink:
I might change it in the future. For now, I am trying to get React to work for me :dark_sunglasses:

FYI I posted this question on SO: https://stackoverflow.com/questions/56441647/how-can-i-chain-dynamic-imports-with-meteor