Les Promises JS dans une autre promise dans Array.map()

Le problème exposé dans ce sujet a été résolu.

Bonsoir !

Oui le titre est un peu compliqué mais je suis bloqué sur ce problème depuis 2 jours… :D

J’ai une classe Storage qui me permet d’accéder aux informations d’un fichier via une méthode _getFile() que voici :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
    static _getFile(url, stats) {
        return new Promise((resolve, reject) => {
            const path = _path.join(Storage.root, url);
            const path_parsed = _path.parse(path);

            db.File.findOne({where: {path: path}, include: ['owner', 'group']})
                .then(file => {
                    file.type = 'file';
                    file.url = Storage.url + url;
                    file.base = path_parsed.base;
                    file.name = path_parsed.name;
                    file.ext = path_parsed.ext;
                    file.size = stats.size;
                    file.mime = mime.lookup(path_parsed.ext);

                    console.log('File ready');
                    return resolve(file);
                })
                .catch(err => {
                    return reject(err);
                });
        });
    };

Et une autre qui fait la même mais pour un dossier (pas besoin d’appel à une base de donnée) :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
    static _getDirectory(url) {
        return new Promise(resolve => {
            const path = _path.join(Storage.root, url);

            console.log('Directory ready');
            return resolve({
                type: 'directory',
                name: _path.basename(path),
                url: Storage.url + '/' + url,
                path: path,
            });
        });
    };

Si une requête réclamant un fichier est reçue, la première méthode est utilisé, tout fonctionne bien. Si par contre une requête réclame un dossier, c’est là où ça se complique car il me faut créer l’arborescence de ce dossier. Voici la méthode qui s’en charge :

 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
    static _getDirectoryTree(url) {
        return new Promise(resolve => {
            Storage._getDirectory(url)
                .then(dir => {
                    dir.children = {};

                    Storage._safeReadDir(dir.path)
                        .then(dirData => {
                            if (dirData !== null) {
                                dir.children = dirData.map(child => {
                                    const child_url = url + '/' + child;

                                    Storage._getStats(child_url)
                                        .then(stats => {
                                            if (stats.isDirectory()) {
                                                Storage._getDirectory(child_url)
                                                    .then(directory => directory);
                                            }
                                            else {
                                                Storage._getFile(child_url, stats)
                                                    .then(file => file);
                                            }
                                        });
                                }); // endmap
                            } // endif
                        })
                        .then(() => {
                            console.log('returning the dir');
                            return resolve(dir);
                        });
                });
        });
    };

Je suis désolé pour le code un peu long et complexe, j’ai essayé de le simplifier au maximum… Comme vous pouvez le voir, il y a plusieurs console.log() qui me permet de savoir à quel moment les méthodes sont exécutées. Voici ce que j’obtiens :

1
2
3
4
5
Directory ready
returning the dir
Directory ready
Directory ready
File ready

Ce qui n’est évidemment pas dans le bon ordre. Ça signifie que la méthode _getTreeDirectory renvoie le résultant avant que la méthode map est fini son travail, ce qui n’est pas bon ! Je sais que la solution va se trouver au niveau de Promise.all() mais je n’arrive pas à l’utiliser… J’ai fait un gist avec toute la classe dedans pour que ce soit plus clair.

Merci d’avoir tout lu et bonne soirée ! :)

Salut,

_getFile est très simplifiable :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
    static _getFile(url, stats) {
        const path = _path.join(Storage.root, url);
        const path_parsed = _path.parse(path);

        return db.File.findOne({where: {path: path}, include: ['owner', 'group']})
            .then(file => {
                file.type = 'file';
                file.url = Storage.url + url;
                file.base = path_parsed.base;
                file.name = path_parsed.name;
                file.ext = path_parsed.ext;
                file.size = stats.size;
                file.mime = mime.lookup(path_parsed.ext);

                console.log('File ready');
                return file;
            })
    };

Tu n’as pas besoin de retourner une promise du tout dans _getDirectory() (il n’y a que des appels à des fonctions bloquantes dedans) :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
    static _getDirectory(url) {
        const path = _path.join(Storage.root, url);

        console.log('Directory ready');
        return {
            type: 'directory',
            name: _path.basename(path),
            url: Storage.url + '/' + url,
            path: path,
        };
    };

_getDirectoryTree() est également très simplifiable. Les .then(directory => directory); ne servent strictement à rien. Il ne faut pas écrire des pyramides comme ça, c’est pénible à lire et c’est mal.

Aussi, modifier dir.children comme tu le fais n’est pas une très bonne idée à mon goût.

Je t’invite à arranger et simplifier _getDirectoryTree() et à relire de la doc sur les promises :).

+1 -0

Merci, mon code est déjà un peu plus lisible. Je sais qu’il faut éviter à tout prix les pyramides mais je vois mal comment faire autrement.

Quand tu me dis que je peux améliorer ma méthode _getDirectoryTree(), tu parles de chaîner un peu plus en utilisant les .then() ? Car je vois pas comment faire avec map()… Je suis obligé d’imbriquer non ?

Les .then(directory => directory); ne servent strictement à rien.

Maintenant que _getDirectory n’utilise plus les promise oui, je suis d’accord. Mais comme _getFile() utilise le système de promise, comment je fais pour retourner la valeur renvoyée par cette méthode sans utiliser la syntaxe .then(file => file) ?

EDIT : J’ai enfin réussi à avoir un résultat qui fonctionne ! Après je sais pas si c’est super propre, j’attend vos conseils. Voici donc ma nouvelle méthode 100% fonctionnelle :

 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
    static _getDirectoryTree(url) {
        return new Promise(resolve => {
            let dir = Storage._getDirectory(url);
            const dirData = fs.readdirSync(dir.path);

            let promises = [];

            if (dirData !== null) {
                promises = dirData.map(child => {
                    const child_url = url + '/' + child;

                    return Storage._getStats(child_url)
                        .then(stats => {
                            if (stats.isDirectory()) {
                                return Storage._getDirectory(child_url);
                            }
                            else {
                                return Storage._getFile(child_url, stats);
                            }
                        });
                });
            }

            Promise.all(promises).then((res) => {
                dir.children = res;
                return resolve(dir);
            });
        });
    };
+0 -0

Alors en gros, une promise encapsule une valeur (les haskelleux vont crier « C’EST PARCE QUE C’EST UNE MONADE », d’ailleurs, je les entends d’ici, bref).

Donc amusons-nous avec ça dans le REPL de node (en mode interactif quoi). Crééons une promise résolue et affichons sa valeur encapsulée :

1
2
3
4
5
6
> (new Promise(resolve => resolve('coucou'))).then(console.log)
Promise {
  <pending>,
  
}
'coucou'

Ça marche. Maintenant, rajoutons des .then(v => v) :

1
2
3
4
5
6
> (new Promise(resolve => resolve('coucou'))).then(v => v).then(v => v).then(v => v).then(console.log)
Promise {
  <pending>,
  
}
'coucou'

Ça ne change rien, ça marche exactement pareil. Les .then(v => v) sont donc inutiles :p.

D’une certaine manière, on peut dire que then() sert à deux choses² :

  • Exécuter du code sans véritable rapport avec la promise en soi lorsque la promise est résolue (exactement comme un bon vieux callback quoi) ;

  • Transformer la valeur encapsulée par la promise, en retournant la nouvelle valeur dans la fonction passée en paramètre à then()¹. Et ça, c’est spécial :

1
2
3
4
5
6
> (new Promise(resolve => resolve('coucou'))).then(v => v + ' toi').then(console.log)
Promise {
  <pending>,
  
}
'coucou toi'

(Notez que le jour (proche) où les mots-clés async et await seront supportés par le REPL de Node.js, ces exemples pourront être beaucoup plus jolis)


¹ : On peut aussi retourner une promise de façon à ce qu’elle soit mise “à la suite”, mais j’explique ça mal et je n’ai pas envie de faire l’effort d’expliquer ça bien. Veuillez vous référer à de la vraie documentation fiable, pas à ce commentaire écrit en 5 minutes :)

² : Après avoir écrit ça, je trouve que ce découpage en deux manière distinces d’utiliser les promises est assez ridicule en fait. Mais j’ai la fleimme de chercher une manière d’expliquer les choses autrement.


EDIT : Je viens de voir (après avoir posté) que tu as trouvé une solution à ton problème en simplifiant, et c’est cool :) Retirer les modifications du dir.children = {}; est clairement une bonne chose.


EDIT 2 : Tu peux encore simplifier en retirant le new Promise({… et en retournant Promise.all(promises).then((res) => {…, exactement comme j’ai fait pour la première fonction.

+2 -0
Connectez-vous pour pouvoir poster un message.
Connexion

Pas encore membre ?

Créez un compte en une minute pour profiter pleinement de toutes les fonctionnalités de Zeste de Savoir. Ici, tout est gratuit et sans publicité.
Créer un compte