[js] Hoe kan ik een Promise meerdere keren uitvoeren?

Pagina: 1
Acties:

Vraag


Acties:
  • 0 Henk 'm!

  • thomas_24_7
  • Registratie: Januari 2003
  • Niet online
Voor een vriend maak ik een simpel programmatje in Node.js wat een JSON result moet ophalen bij een API. Over deze API heb ik geen controle. Het ophalen doe ik middels het NPM package node-fetch.

Als ik dit éénmaal wil uitvoeren is dit geen probleem. De API retourneert echter maximaal 100 objecten per request, dus voor een volledige data-set moet ik meerdere requests uitvoeren. In de url kan je dan de parameter offset meegeven (?offset=x), om de eerste x aantal objecten over te slaan en de volgende 100 te retourneren.

Nu loop ik tegen het concept Promises aan en zou dit graag willen inzetten om geen chaotische callback-hell te schrijven. Maar het begint mij een beetje te duizelen. De requests zijn dan wel asynchroon, ze moeten wél sequentieel worden uitgevoerd. Immers weet ik pas bij de response (wanneer deze 100 objecten bevat) of ik nog meer objecten op moet halen. Hoe ga ik dit in de juiste Promise vorm gieten? Het probleem is dus de Promise syntax/werking, zonder dat het platform er echt toe doet.

Het beste voorbeeld wat ik heb kunnen vinden is dit: Print results one by one, slower overall.
Wat weer afgeleid is van het volgende voorbeeld: Creating a sequence.

Echter is bij beide voorbeelden vooraf bekend hoeveel requests plaats moeten vinden en is het bij mij afhankelijk van de response. Hoe kan ik een Promise een vooraf onbekend aantal keer herhalen?

Denk ik nu helemaal fout en is dit niet mogelijk of kan iemand mij het juiste pad wijzen? Wellicht aan de hand van de genoemde voorbeelden?

Duizend maal dank!

Beste antwoord (via thomas_24_7 op 23-10-2016 21:49)


  • RobIII
  • Registratie: December 2001
  • Niet online

RobIII

Admin Devschuur®

^ Romeinse Ⅲ ja!

(overleden)
Rem schreef op donderdag 20 oktober 2016 @ 11:39:
Even heel kort door de bocht, code zal wel niet eens werken.
Nee, want zo werkt asynchrone code niet ;) (En dat is nou nét wat Promises oplossen proberen op te lossen :Y) )
thomas_24_7 schreef op vrijdag 21 oktober 2016 @ 00:38:
Nope, dat gaat niet. Ik heb je code wel een beetje aangepast om het werkend te krijgen, dus weet niet of het nog aan je oorspronkelijke idee voldoet, maar voer het maar eens uit in je webconsole.
Maak alsjeblieft dat je van dit ingeslagen pad af komt. Het énige wat je moet doen is in the .Then() bepalen of je nog meer wil ophalen en, zo ja, dezelfde promise nogmaals invoken met offset + batchsize argument. C'est tout.

[ Voor 49% gewijzigd door RobIII op 21-10-2016 00:51 ]

There are only two hard problems in distributed systems: 2. Exactly-once delivery 1. Guaranteed order of messages 2. Exactly-once delivery.

Je eigen tweaker.me redirect

Over mij

Alle reacties


Acties:
  • 0 Henk 'm!

  • RobIII
  • Registratie: December 2001
  • Niet online

RobIII

Admin Devschuur®

^ Romeinse Ⅲ ja!

(overleden)
Kijk in je callback of je 100 objecten hebt gehad, zo ja, dan nog een call doen. Zo nee, dan was dat de 'laatste pagina'. Stukje recursie (in je .then() bijvoorbeeld) et voila. Oh, ik zie net dat je dat al wel door had :P

Maar laat eens zien welke code je nu hebt (beperk 't aub. tot de relevante regels code)?

[ Voor 47% gewijzigd door RobIII op 19-10-2016 01:12 ]

There are only two hard problems in distributed systems: 2. Exactly-once delivery 1. Guaranteed order of messages 2. Exactly-once delivery.

Je eigen tweaker.me redirect

Over mij


Acties:
  • 0 Henk 'm!

  • Hydra
  • Registratie: September 2000
  • Laatst online: 21-08 17:09
thomas_24_7 schreef op woensdag 19 oktober 2016 @ 00:32:
Denk ik nu helemaal fout en is dit niet mogelijk of kan iemand mij het juiste pad wijzen? Wellicht aan de hand van de genoemde voorbeelden?
Een beetje wel. Een promise is eigenlijk niks meer dan een wrapper objectje met daarin een callback die wordt aangeroepen op 't moment dat je een resultaat hebt (of een error krijgt).

Als jij code wil maken die een promise retourneert naar een resultaat dat opgehaald moet worden met meerdere async calls zul je dat zelf moeten implementeren. Je maakt dus je eigen promise and gaat in je eigen code die promise pas de 'okay' geven als je alle data binnen hebt.

https://niels.nu


Acties:
  • 0 Henk 'm!

  • thomas_24_7
  • Registratie: Januari 2003
  • Niet online
Ik heb getracht een summiere demo met comments te maken...niet werkend of course, maar het dekt wel de lading geloof ik:

JavaScript:
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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
// Import modules
let Fetch = require('node-fetch');

let ApiHandler = function()
{
    let offset                  = 0;
    let getMoreOrdersPromises   = new Array();

    this.getOrders = function()
    {
        let chain               = Promise.resolve();    // Initialize to empty resolved promise, might need this later to chain Promises
        let orderAccumulator    = new Array();

        // This stuff needs to be dynamically repeated though...doing it twice manually for example's sake.
        getMoreOrdersPromises.push(getMoreOrders());
        getMoreOrdersPromises.push(getMoreOrders());
        getMoreOrdersPromises.forEach(getMoreOrdersPromise => {
            // chain = chain.then(getMoreOrdersPromise);
            let processedOrders = chain.then(getMoreOrdersPromise);
            orderAccumulator    = orderAccumulator.concat(processedOrders);
        });

        return orderAccumulator;
    };

    function getMoreOrders()
    {
        return new Promise(
            function(resolve, reject)
            {
                // This Fetch should somehow be promisified and repeated
                Fetch(getOrdersUri())
                    .then((res) => {
                        return res.json();
                    })
                    .then((orders) => {
                        if(orders.length == 100)
                        {
                            offset += 100;
                        }

                        // Maybe I need a way to add another getMoreOrders Promise to the promiseFactories array here? Something like:
                        // getMoreOrdersPromises.push(this.getMoreOrders);
                        // But idk if the forEach is gonna pick it up once it's started iterating.

                        resolve(processOrders(orders)); // processOrders could also be async I guess, but not in the mood now.
                    })
                    .catch((err) => {
                        console.log(err);
                        reject(err);    // Is this ok?
                    });
            }
        );
    };

    function getOrdersUri()
    {
        // Simplified
        return 'https://api.site.com/orders/?since=2016-09-01&offset=' + offset;
    }
    
    function processOrders(orders)
    {
        let processedOrders = new Array();

        for (let order of orders)
        {
            // Do some magical processing to orders
            processedOrders.push(order);
        }

        return processedOrders;
    }
}

module.exports = new ApiHandler();


Startpunt van de code is ApiHandler.getOrders();

Ik sta open voor alle op/aanmerkingen overigens. Mijn codes zien zelden een peer-review :'(

[ Voor 5% gewijzigd door thomas_24_7 op 19-10-2016 23:46 ]


Acties:
  • +1 Henk 'm!

  • Rem
  • Registratie: Oktober 2005
  • Laatst online: 10:56

Rem

Even heel kort door de bocht, code zal wel niet eens werken. Maar je kunt toch gewoon zoiets doen?
JavaScript:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
var allOrders = []
var offset = 0;
var repeat = true;
while(repeat){
    var orders;
    GetOrders(offset).then(function(data){
        orders = data;      
    }); 

    allOrders.push(orders);
    if(orders.length < 100){
        repeat = false;
    }
}

Acties:
  • 0 Henk 'm!

  • thomas_24_7
  • Registratie: Januari 2003
  • Niet online
Rem schreef op donderdag 20 oktober 2016 @ 11:39:
Even heel kort door de bocht, code zal wel niet eens werken. Maar je kunt toch gewoon zoiets doen?
Nope, dat gaat niet. Ik heb je code wel een beetje aangepast om het werkend te krijgen, dus weet niet of het nog aan je oorspronkelijke idee voldoet, maar voer het maar eens uit in je webconsole. Ik gebruik een setTimeout om een asynchrone call (naar de API) te simuleren. Zie mijn comments.

JavaScript:
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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
var allOrders       = new Array();
var count           = 0;
var hasMoreOrders   = true;
var offset          = 0;

function promiseGenerator(offset)
{
    return new Promise(
        function (resolve, reject)
        {
            let timeOut = ((Math.floor(Math.random() * 10) + 1) * 1000);
            console.log('wait is ' + timeOut + ' seconds.');
          
            window.setTimeout(
                function()
                {
                    // I want to simulate that the API has ~586 results
                    let data = offset < 500 ? new Array(100) : new Array(86);

                    console.log('returned range: ' + offset + '-' + data.length);
                    resolve(data);
                },
                timeOut
            );
        }
    );
}

// While loop is cpu intensive
while(hasMoreOrders)
{
    var orders;

    //Safety measure to prevent eternal-loop
    if(count < 20) 
    {
      count++;
    }
  
    else
    {
      break;
    }

    // Offset will stay 0 while creating Promises because callback wouldn't have fired/returned yet
    promiseGenerator(offset).then(function(orders)
    {
        allOrders = allOrders.concat(orders);
        console.log('Promise fulfilled. Total orders: ' + allOrders.length);
      
        if(orders.length < 100)
        {
            // When this statement is reached, the while loop will have ran thousands of times.
            // Probably will not influence the while loop anyway.
            hasMoreOrders = false;
        }

        else
        {
            offset += 100;
        }
    });
}

Acties:
  • Beste antwoord
  • +1 Henk 'm!

  • RobIII
  • Registratie: December 2001
  • Niet online

RobIII

Admin Devschuur®

^ Romeinse Ⅲ ja!

(overleden)
Rem schreef op donderdag 20 oktober 2016 @ 11:39:
Even heel kort door de bocht, code zal wel niet eens werken.
Nee, want zo werkt asynchrone code niet ;) (En dat is nou nét wat Promises oplossen proberen op te lossen :Y) )
thomas_24_7 schreef op vrijdag 21 oktober 2016 @ 00:38:
Nope, dat gaat niet. Ik heb je code wel een beetje aangepast om het werkend te krijgen, dus weet niet of het nog aan je oorspronkelijke idee voldoet, maar voer het maar eens uit in je webconsole.
Maak alsjeblieft dat je van dit ingeslagen pad af komt. Het énige wat je moet doen is in the .Then() bepalen of je nog meer wil ophalen en, zo ja, dezelfde promise nogmaals invoken met offset + batchsize argument. C'est tout.

[ Voor 49% gewijzigd door RobIII op 21-10-2016 00:51 ]

There are only two hard problems in distributed systems: 2. Exactly-once delivery 1. Guaranteed order of messages 2. Exactly-once delivery.

Je eigen tweaker.me redirect

Over mij


Acties:
  • +1 Henk 'm!

  • Rem
  • Registratie: Oktober 2005
  • Laatst online: 10:56

Rem

RobIII schreef op vrijdag 21 oktober 2016 @ 00:48:
[...]

Nee, want zo werkt asynchrone code niet ;) (En dat is nou nét wat Promises oplossen proberen op te lossen :Y) )

[...]

Maak alsjeblieft dat je van dit ingeslagen pad af komt. Het énige wat je moet doen is in the .Then() bepalen of je nog meer wil ophalen en, zo ja, dezelfde promise nogmaals invoken met offset + batchsize argument. C'est tout.
Edit: ik zie nu wat je bedoelt, sorry. Voor Thomas, je kunt gewoon recursive gebruiken. Dus een promise returnt in dit geval een promise, die weer een promise kan returnen OF resolven. Indien hij resolved resolven alle promises ervoor natuurlijk ook.

JavaScript:
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
var allOrders = [];

function promiseGenerator(offset)
{
    return new Promise(function (resolve, reject){
        window.setTimeout(
            function()
            {
                // I want to simulate that the API has ~586 results
                let data = offset < 500 ? new Array(100) : new Array(86);
                allOrders.concat(data);

                if(data.length < 100){
                    resolve();
                } else{
                    return promiseGenerator(offset + 100)
                }
            }, 200
        );
    });
}

promiseGenerator(0).then(function(){
    alert(allOrders);
})


Het ging dan ook meer om het concept, het probleem hier zit hem meer in het niet helemaal zien hoe deze code ontworpen moet worden dan hoe de code uberhaupt werkt. Volgens mij snapt Thomas dat wel redelijk.

Mijn flow is eigenlijk precies wat jij voorstelt. De results dus ophalen, checken of je er nog meer moet hebben of niet en dan eventueel dus de promise nog eens inschieten.

[ Voor 34% gewijzigd door Rem op 21-10-2016 10:45 ]


Acties:
  • 0 Henk 'm!

  • thomas_24_7
  • Registratie: Januari 2003
  • Niet online
RobIII schreef op vrijdag 21 oktober 2016 @ 00:48:
Maak alsjeblieft dat je van dit ingeslagen pad af komt. Het énige wat je moet doen is in the .Then() bepalen of je nog meer wil ophalen en, zo ja, dezelfde promise nogmaals invoken met offset + batchsize argument. C'est tout.
Bedankt Rob! Dit was het zetje wat ik nodig had. Samen met het voorbeeld van Rem en Google begint het mij eindelijk duidelijk te worden. Super gaaf die Promises en die andere JS nieuwigheden. Levert echt mooie, overzichtelijke code op. Zo heb ik ook de allOrders variabele uit de bovenliggende scope weggewerkt, net iets cleaner denk ik.

Ohja en ik was niet echt "dat pad ingeslagen" hoor, maar wilde even een educatief voorbeeld aanleveren. Bij deze ook mijn laatste concept, misschien dat een ander die worstelt met Promises er nog iets aan heeft:

JavaScript:
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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
var limit = 100;    // Limit number of results

// Using default values for parameters
function getOrders(offset = 0, previousOrders = new Array())
{
    // Generate and return a Promise. Using an arrow function as constructor
    return new Promise((resolve, reject) =>
    {
        let timeOut = ((Math.floor(Math.random() * 4) + 1));        // Random integer between 1 and 4

        console.log('Simulating async action...waiting ' + timeOut + ' seconds.');

        window.setTimeout(
            function()
            {
                let orders          = offset < 500 ? new Array(limit) : new Array(86);  // Simulate API results (max 586 in this case)
                let collectedOrders = previousOrders.concat(orders);                    // Combine results with previous results

                console.log('Received range: ' + offset + '-' + collectedOrders.length);

                // 1/10 fail rate to demonstrate error/reject
                if(Math.floor(Math.random() * 10) == 0)
                {
                    return reject(new Error('Connection lost.'));   // Using explicit return to avoid run-to-completion of code (risk running into a resolve too)
                }

                else if(orders.length < limit)
                {
                    console.log('Received less than limit (' + limit + '). This were the last results!');

                    return resolve(collectedOrders);
                }

                else
                {
                    console.log('Get ' + limit + ' more!');

                    return resolve(getOrders(offset + limit, collectedOrders)); // Where the magic happens: resolve with another Promise
                }
            },
            timeOut * 1000
        );
    });
}

getOrders()
  .then((resolved) =>
    {
        console.log('Promise fulfilled. Total orders: ' + resolved.length);
    })
  .catch((error) => // Prefer catch over then(onFulfilled, onRejected) because it also catches errors from onFulfilled
    {
        console.log(error);
    });


@Rem: betreffende je demo, let op dat Array.concat de gecombineerde arrays returned en niet de array waarop je het aanroept wijzigt!

[ Voor 5% gewijzigd door thomas_24_7 op 23-10-2016 21:51 ]

Pagina: 1