Await responses for multiple makeWebRequest calls

HI,

I'm trying to build a widget that communicates with an API. To get all the data I would like to show I need to do multiple calls to the API. The first is to get some general data (users in my example). For each user I need to make a request to get more info (unlike the sample API I used, the API I talk to has no ability to return this information in one call).

I build to below example based on the WebRequest sample from the SDK.

//
// Copyright 2016 by Garmin Ltd. or its subsidiaries.
// Subject to Garmin SDK License Agreement and Wearables
// Application Developer Agreement.
//

using Toybox.Communications;
using Toybox.System;
using Toybox.WatchUi;

class WebRequestDelegate extends WatchUi.BehaviorDelegate {
var notify;
var i = 1;

// Set up the callback to the view
function initialize(handler) {
WatchUi.BehaviorDelegate.initialize();
notify = handler;

}

function log(msg) {
System.println(i + ". " + msg);
i++;
}

// Handle menu button press
function onMenu() {
makeRequest();
return true;
}

function onSelect() {
makeRequest();
return true;
}

function makeRequest() {
notify.invoke("Executing\nRequest");

log("makeRequest executed");

var url = "jsonplaceholder.typicode.com/users";

log("makeWebRequest to " + url);

Communications.makeWebRequest(url, {}, {}, method(:onReceive));
}

// Receive the data from the web request
function onReceive(responseCode, data) {
log("onReceive executed");
log("data: " + data);

for (var x = 0; x < 3; x++) {
makeSingleUserRequest(data[x]["id"]);
}

log("all user data collected. now I would like to show the data in my view");
}

function makeSingleUserRequest(userId) {
notify.invoke("Executing\nRequest");

log("makeSingleUserRequest executed");

var url = "jsonplaceholder.typicode.com/.../" + userId;

log("makeWebRequest to " + url);

Communications.makeWebRequest(url, {}, {}, method(:onReceiveSingleUser));
}

// Receive the data from the web request
function onReceiveSingleUser(responseCode, data) {
log("onReceiveSingleUser executed");
log("data: " + data);
log("username: " + data["username"]);

if (data["username"].toString() == "Bret") {
log("Hi Bret");
} else {
log("Hi guest");
}
}
}


If you run this code it will output the following console log

1. makeRequest executed
2. makeWebRequest to jsonplaceholder.typicode.com/users
3. onReceive executed
4. data: [{address=>{geo=>{lng=>81.1496, lat=>-37.3159}, suite=>Apt. 556, zipcode=>92998-3874, city=>Gwenborough, street=>Kulas Light}, name=>Leanne Graham, id=>1, company=>{name=>Romaguera-Crona, catchPhrase=>Multi-layered client-server neural-net, bs=>harness real-time e-markets}, phone=>1-770-736-8031 x56442, email=>[email protected], username=>Bret, website=>hildegard.org}, {address=>{geo=>{lng=>-34.4618, lat=>-43.9509}, suite=>Suite 879, zipcode=>90566-7771, city=>Wisokyburgh, street=>Victor Plains}, name=>Ervin Howell, id=>2, company=>{name=>Deckow-Crist, catchPhrase=>Proactive didactic contingency, bs=>synergize scalable supply-chains}, phone=>010-692-6593 x09125, email=>[email protected], username=>Antonette, website=>anastasia.net}, {address=>{geo=>{lng=>-47.0653, lat=>-68.6102}, suite=>Suite 847, zipcode=>59590-4157, city=>McKenziehaven, street=>Douglas Extension}, name=>Clementine Bauch, id=>3, company=>{name=>Romaguera-Jacobson, catchPhrase=>Face to face bifurcated interface, bs=>e-enable strategic applications}, phone=>1-463-123-4447, email=>[email protected], username=>Samantha, website=>ramiro.info}, {address=>{geo=>{lng=>-164.2990, lat=>29.4572}, suite=>Apt. 692, zipcode=>53919-4257, city=>South Elvis, street=>Hoeger Mall}, name=>Patricia Lebsack, id=>4, company=>{name=>Robel-Corkery, catchPhrase=>Multi-tiered zero tolerance productivity, bs=>transition cutting-edge web services}, phone=>493-170-9623 x156, email=>[email protected], username=>Karianne, website=>kale.biz}, {address=>{geo=>{lng=>62.5342, lat=>-31.8129}, suite=>Suite 351, zipcode=>33263, city=>Roscoeview, street=>Skiles Walks}, name=>Chelsey Dietrich, id=>5, company=>{name=>Keebler LLC, catchPhrase=>User-centric fault-tolerant solution, bs=>revolutionize end-to-end systems}, phone=>(254)954-1289, email=>[email protected], username=>Kamren, website=>demarco.info}, {address=>{geo=>{lng=>71.7478, lat=>-71.4197}, suite=>Apt. 950, zipcode=>23505-1337, city=>South Christy, street=>Norberto Crossing}, name=>Mrs. Dennis Schulist, id=>6, company=>{name=>Considine-Lockman, catchPhrase=>Synchronised bottom-line interface, bs=>e-enable innovative applications}, phone=>1-477-935-8478 x6430, email=>[email protected], username=>Leopoldo_Corkery, website=>ola.org}, {address=>{geo=>{lng=>21.8984, lat=>24.8918}, suite=>Suite 280, zipcode=>58804-1099, city=>Howemouth, street=>Rex Trail}, name=>Kurtis Weissnat, id=>7, company=>{name=>Johns Group, catchPhrase=>Configurable multimedia task-force, bs=>generate enterprise e-tailers}, phone=>210.067.6132, email=>[email protected], username=>Elwyn.Skiles, website=>elvis.io}, {address=>{geo=>{lng=>-120.7677, lat=>-14.3990}, suite=>Suite 729, zipcode=>45169, city=>Aliyaview, street=>Ellsworth Summit}, name=>Nicholas Runolfsdottir V, id=>8, company=>{name=>Abernathy Group, catchPhrase=>Implemented secondary concept, bs=>e-enable extensible e-tailers}, phone=>586.493.6943 x140, email=>[email protected], username=>Maxime_Nienow, website=>jacynthe.com}, {address=>{geo=>{lng=>-168.8889, lat=>24.6463}, suite=>Suite 449, zipcode=>76495-3109, city=>Bartholomebury, street=>Dayna Park}, name=>Glenna Reichert, id=>9, company=>{name=>Yost and Sons, catchPhrase=>Switchable contextually-based project, bs=>aggregate real-time technologies}, phone=>(775)976-6794 x41206, email=>[email protected], username=>Delphine, website=>conrad.com}, {address=>{geo=>{lng=>57.2232, lat=>-38.2386}, suite=>Suite 198, zipcode=>31428-2261, city=>Lebsackbury, street=>Kattie Turnpike}, name=>Clementina DuBuque, id=>10, company=>{name=>Hoeger LLC, catchPhrase=>Centralized empowering task-force, bs=>target end-to-end models}, phone=>024-648-3804, email=>[email protected], username=>Moriah.Stanton, website=>ambrose.net}]
5. makeSingleUserRequest executed
6. makeWebRequest to jsonplaceholder.typicode.com/.../1
7. makeSingleUserRequest executed
8. makeWebRequest to jsonplaceholder.typicode.com/.../2
9. makeSingleUserRequest executed
10. makeWebRequest to jsonplaceholder.typicode.com/.../3
11. all user data collected. now I would like to show the data in my view
12. onReceiveSingleUser executed
13. data: {address=>{geo=>{lng=>-47.0653, lat=>-68.6102}, suite=>Suite 847, zipcode=>59590-4157, city=>McKenziehaven, street=>Douglas Extension}, name=>Clementine Bauch, id=>3, company=>{name=>Romaguera-Jacobson, catchPhrase=>Face to face bifurcated interface, bs=>e-enable strategic applications}, phone=>1-463-123-4447, email=>[email protected], username=>Samantha, website=>ramiro.info}
14. username: Samantha
15. Hi guest
16. onReceiveSingleUser executed
17. data: {address=>{geo=>{lng=>-34.4618, lat=>-43.9509}, suite=>Suite 879, zipcode=>90566-7771, city=>Wisokyburgh, street=>Victor Plains}, name=>Ervin Howell, id=>2, company=>{name=>Deckow-Crist, catchPhrase=>Proactive didactic contingency, bs=>synergize scalable supply-chains}, phone=>010-692-6593 x09125, email=>[email protected], username=>Antonette, website=>anastasia.net}
18. username: Antonette
19. Hi guest
20. onReceiveSingleUser executed
21. data: {address=>{geo=>{lng=>81.1496, lat=>-37.3159}, suite=>Apt. 556, zipcode=>92998-3874, city=>Gwenborough, street=>Kulas Light}, name=>Leanne Graham, id=>1, company=>{name=>Romaguera-Crona, catchPhrase=>Multi-layered client-server neural-net, bs=>harness real-time e-markets}, phone=>1-770-736-8031 x56442, email=>[email protected], username=>Bret, website=>hildegard.org}
22. username: Bret
23. Hi guest


I'm wondering how I can make #11 for the console log the last log to be written. Is there an easy trick or do I need to build my own tracker/counter to find out for how many users have the additinal info?

There is another funny thing. You might notice the if/else in the onReceiveSingleUser method. On rule #23 in the console log you see "Hi guest", but it should say "Hi Bret". How do I do this? I've tried multiple things with toString(), toCharArray(), toUtf8Array() or .equals(), but somehow it does not validate the if-statement...

I hope you can help me!
  • With the "Bret" question, you need to use equals() with stings. so
    if (data["username"].equals("Bret") {


    Seems for #11, you want to keep a counter of the number of requests you send, and in the onReceive for that, wait until you'd gotten that many responses.

    Note: I recall you can only have 3 outstanding makeWebRequests at a time, so based on your app, you maybe have to re-work this logic in general. You'll also what to check the response code when you get data, as "data" could be null.
  • I was testing something last summer, and I recall having an issue with queueing up 3 with Android, and I ended up single threading the requests
    so, request1
    then request 2 in onReceive of #1
    then request 3 in onReceive of #2

    One thing about testing makeWebRequest, is in general things happen much faster in the sim than when using a device and a phone. And if there's a timeout error, they can take even longer.
  • If you have the resources available, creating 'command' objects and an 'execution queue' seems like a decent solution. This is a fairly common programming pattern.

    class Command
    {
    var _next;
    var _queue;

    function initialize() {
    _next = null;
    _queue = null;
    }

    function start() {
    }

    function finish() {
    var queue = _queue;
    _queue = null;

    if (queue != null) {
    queue.finish_command();
    }
    }
    }

    class CommandExecutor
    {
    hidden var _head;
    hidden var _tail;

    function initialize() {
    _head = null;
    _tail = null;
    }

    function add_command(command) {
    command._queue = self;

    if (_head == null) {
    _head = command;
    _tail = command;

    command.start();
    }
    else {
    _tail._next = command;
    _tail = command;
    }
    }

    function finish_command() {
    // remove the front item in the queue
    var head = _head;
    _head = head._next;
    head._next = null;
    head._queue = null;

    // now _head is null or references the next command
    if (_head == null) {
    _tail = null;
    }
    else {
    _head.start();
    }
    }
    }


    Then you can create derived 'commands', like this..

    using Toybox.System;
    using Toybox.Communications;

    // command that wraps a function taking one argument
    class UnaryCommand extends Command
    {
    hidden var _callback;
    hidden var _param1;

    function initialize(callback, param1) {
    Command.initialize();

    _callback = callback;
    _param1 = param1;
    }

    function start() {
    _callback.invoke(_param1);
    _callback = null;

    Command.finish();
    }
    }

    // command that makes a web request
    class WebRequestCommand extends Command
    {
    hidden var _url;
    hidden var _params;
    hidden var _options;
    hidden var _callback;

    function initialize(url, params, options, callback) {
    Command.initialize();
    _url = url;
    _params = params;
    _options = options;
    _callback = callback;
    }

    function start() {
    Communications.makeWebRequest(_url, _params, _options, self.method(:handle_response));
    }

    function handle_response(code, data) {
    _callback.invoke(code, data);
    _callback = null;

    // remove self from the queue, start the next request
    Command.finish();
    }
    }


    And then you'd put it all together like this:

    hidden var _queue;

    function initialize() {
    BehaviorDelegate.initialize();

    _queue = new CommandExecutor();
    }

    function onSelect() {
    var params = {
    };

    var options = {
    };

    _queue.add_command(new UnaryCommand(self.method(:print), "0"));
    _queue.add_command(new WebRequestCommand("jsonplaceholder.typicode.com/.../1", params, options, self.method(:onWebResponse)));
    _queue.add_command(new UnaryCommand(self.method(:print), "1"));
    _queue.add_command(new WebRequestCommand("jsonplaceholder.typicode.com/.../2", params, options, self.method(:onWebResponse)));
    _queue.add_command(new UnaryCommand(self.method(:print), "2"));
    _queue.add_command(new WebRequestCommand("jsonplaceholder.typicode.com/.../3", params, options, self.method(:onWebResponse)));
    _queue.add_command(new UnaryCommand(self.method(:print), "3"));

    return true;
    }

    function onWebResponse(code, data) {
    if (code == 200) {
    System.println(Lang.format("onWebResponse($1$, $2$)", [ code, data["name"] ]));
    }
    else {
    System.println(Lang.format("onWebResponse($1$)", [ code ]));
    }
    }

    function print(param) {
    System.println(param);
    }


    The output looks like this:

    0
    onWebResponse(200, Leanne Graham)
    1
    onWebResponse(200, Ervin Howell)
    2
    onWebResponse(200, Clementine Bauch)
    3
  • Thanks guys, this is really helpful! I'm going to play with this tonight.

    With the "Bret" question, you need to use equals() with stings. so
    if (data["username"].equals("Bret") {



    The validating works. I thought I tested this, but apparently not...

    I was testing something last summer, and I recall having an issue with queueing up 3 with Android


    A quick test turned out that the simulator runs up to 4 web requests. Fro the fifth on the requests return response code -101
  • With the 3 concurrent requests, I saw problems with Android on a real device. Someone with iOS ran the same app, and had no problems, and it ran fine in the sim.
  • Thanks again guys! Got it working :D

    I put the first two codeblocks from Travis.ConnectIQ in a file and updated my WebRequestDelegate. Here is my code:

    //
    // Copyright 2016 by Garmin Ltd. or its subsidiaries.
    // Subject to Garmin SDK License Agreement and Wearables
    // Application Developer Agreement.
    //

    using Toybox.Communications;
    using Toybox.System;
    using Toybox.WatchUi;

    class WebRequestDelegate extends WatchUi.BehaviorDelegate {

    var notify;
    var i = 1;
    hidden var _queue;

    // Set up the callback to the view
    function initialize(handler) {
    WatchUi.BehaviorDelegate.initialize();
    notify = handler;
    _queue = new CommandExecutor();
    }

    function log(msg) {
    System.println(i + ". " + msg);
    i++;
    }

    // Handle menu button press
    function onMenu() {
    makeRequest();
    return true;
    }

    function onSelect() {
    makeRequest();
    return true;
    }

    function makeRequest() {
    notify.invoke("Executing\nRequest");

    log("makeRequest executed");

    var url = "jsonplaceholder.typicode.com/users";

    log("makeWebRequest to " + url);

    _queue.add_command(new WebRequestCommand(url, {}, {}, self.method(:onReceive)));
    }

    // Receive the data from the web request

    function onReceive(responseCode, data) {
    log("onReceive executed");
    log("responseCode: " + responseCode);

    if (data != null) {
    log("data: " + data);

    for (var x = 0; x < 5; x++) {
    makeSingleUserRequest(data[x]["id"]);
    }

    _queue.add_command(new UnaryCommand(self.method(:onFinished), "Received\nall users"));
    }
    }

    function makeSingleUserRequest(userId) {
    var url = "jsonplaceholder.typicode.com/.../" + userId;

    log("makeWebRequest to " + url);

    _queue.add_command(new WebRequestCommand(url, {}, {}, self.method(:onReceiveSingleUser)));
    }

    // Receive the data from the web request
    function onReceiveSingleUser(responseCode, data) {
    log("onReceiveSingleUser executed");
    log("responseCode: " + responseCode);

    if (data != null) {
    log("data: " + data);
    log("username: " + data["username"]);

    if (data["username"].equals("Bret")) {
    log("Hi Bret");
    } else {
    log("Hi guest");
    }
    }
    }

    function onFinished(msg) {
    notify.invoke(msg);

    log(msg);
    }

    }


    And the console log:

    1. makeRequest executed
    2. makeWebRequest to jsonplaceholder.typicode.com/users
    3. onReceive executed
    4. responseCode: 200
    5. data: [{address=>{geo=>{lng=>81.1496, lat=>-37.3159}, suite=>Apt. 556, zipcode=>92998-3874, city=>Gwenborough, street=>Kulas Light}, name=>Leanne Graham, id=>1, company=>{name=>Romaguera-Crona, catchPhrase=>Multi-layered client-server neural-net, bs=>harness real-time e-markets}, phone=>1-770-736-8031 x56442, email=>[email protected], username=>Bret, website=>hildegard.org}, {address=>{geo=>{lng=>-34.4618, lat=>-43.9509}, suite=>Suite 879, zipcode=>90566-7771, city=>Wisokyburgh, street=>Victor Plains}, name=>Ervin Howell, id=>2, company=>{name=>Deckow-Crist, catchPhrase=>Proactive didactic contingency, bs=>synergize scalable supply-chains}, phone=>010-692-6593 x09125, email=>[email protected], username=>Antonette, website=>anastasia.net}, {address=>{geo=>{lng=>-47.0653, lat=>-68.6102}, suite=>Suite 847, zipcode=>59590-4157, city=>McKenziehaven, street=>Douglas Extension}, name=>Clementine Bauch, id=>3, company=>{name=>Romaguera-Jacobson, catchPhrase=>Face to face bifurcated interface, bs=>e-enable strategic applications}, phone=>1-463-123-4447, email=>[email protected], username=>Samantha, website=>ramiro.info}, {address=>{geo=>{lng=>-164.2990, lat=>29.4572}, suite=>Apt. 692, zipcode=>53919-4257, city=>South Elvis, street=>Hoeger Mall}, name=>Patricia Lebsack, id=>4, company=>{name=>Robel-Corkery, catchPhrase=>Multi-tiered zero tolerance productivity, bs=>transition cutting-edge web services}, phone=>493-170-9623 x156, email=>[email protected], username=>Karianne, website=>kale.biz}, {address=>{geo=>{lng=>62.5342, lat=>-31.8129}, suite=>Suite 351, zipcode=>33263, city=>Roscoeview, street=>Skiles Walks}, name=>Chelsey Dietrich, id=>5, company=>{name=>Keebler LLC, catchPhrase=>User-centric fault-tolerant solution, bs=>revolutionize end-to-end systems}, phone=>(254)954-1289, email=>[email protected], username=>Kamren, website=>demarco.info}, {address=>{geo=>{lng=>71.7478, lat=>-71.4197}, suite=>Apt. 950, zipcode=>23505-1337, city=>South Christy, street=>Norberto Crossing}, name=>Mrs. Dennis Schulist, id=>6, company=>{name=>Considine-Lockman, catchPhrase=>Synchronised bottom-line interface, bs=>e-enable innovative applications}, phone=>1-477-935-8478 x6430, email=>[email protected], username=>Leopoldo_Corkery, website=>ola.org}, {address=>{geo=>{lng=>21.8984, lat=>24.8918}, suite=>Suite 280, zipcode=>58804-1099, city=>Howemouth, street=>Rex Trail}, name=>Kurtis Weissnat, id=>7, company=>{name=>Johns Group, catchPhrase=>Configurable multimedia task-force, bs=>generate enterprise e-tailers}, phone=>210.067.6132, email=>[email protected], username=>Elwyn.Skiles, website=>elvis.io}, {address=>{geo=>{lng=>-120.7677, lat=>-14.3990}, suite=>Suite 729, zipcode=>45169, city=>Aliyaview, street=>Ellsworth Summit}, name=>Nicholas Runolfsdottir V, id=>8, company=>{name=>Abernathy Group, catchPhrase=>Implemented secondary concept, bs=>e-enable extensible e-tailers}, phone=>586.493.6943 x140, email=>[email protected], username=>Maxime_Nienow, website=>jacynthe.com}, {address=>{geo=>{lng=>-168.8889, lat=>24.6463}, suite=>Suite 449, zipcode=>76495-3109, city=>Bartholomebury, street=>Dayna Park}, name=>Glenna Reichert, id=>9, company=>{name=>Yost and Sons, catchPhrase=>Switchable contextually-based project, bs=>aggregate real-time technologies}, phone=>(775)976-6794 x41206, email=>[email protected], username=>Delphine, website=>conrad.com}, {address=>{geo=>{lng=>57.2232, lat=>-38.2386}, suite=>Suite 198, zipcode=>31428-2261, city=>Lebsackbury, street=>Kattie Turnpike}, name=>Clementina DuBuque, id=>10, company=>{name=>Hoeger LLC, catchPhrase=>Centralized empowering task-force, bs=>target end-to-end models}, phone=>024-648-3804, email=>[email protected], username=>Moriah.Stanton, website=>ambrose.net}]
    6. makeWebRequest to jsonplaceholder.typicode.com/.../1
    7. makeWebRequest to jsonplaceholder.typicode.com/.../2
    8. makeWebRequest to jsonplaceholder.typicode.com/.../3
    9. makeWebRequest to jsonplaceholder.typicode.com/.../4
    10. makeWebRequest to jsonplaceholder.typicode.com/.../5
    11. onReceiveSingleUser executed
    12. responseCode: 200
    13. data: {address=>{geo=>{lng=>81.1496, lat=>-37.3159}, suite=>Apt. 556, zipcode=>92998-3874, city=>Gwenborough, street=>Kulas Light}, name=>Leanne Graham, id=>1, company=>{name=>Romaguera-Crona, catchPhrase=>Multi-layered client-server neural-net, bs=>harness real-time e-markets}, phone=>1-770-736-8031 x56442, email=>[email protected], username=>Bret, website=>hildegard.org}
    14. username: Bret
    15. Hi Bret
    16. onReceiveSingleUser executed
    17. responseCode: 200
    18. data: {address=>{geo=>{lng=>-34.4618, lat=>-43.9509}, suite=>Suite 879, zipcode=>90566-7771, city=>Wisokyburgh, street=>Victor Plains}, name=>Ervin Howell, id=>2, company=>{name=>Deckow-Crist, catchPhrase=>Proactive didactic contingency, bs=>synergize scalable supply-chains}, phone=>010-692-6593 x09125, email=>[email protected], username=>Antonette, website=>anastasia.net}
    19. username: Antonette
    20. Hi guest
    21. onReceiveSingleUser executed
    22. responseCode: 200
    23. data: {address=>{geo=>{lng=>-47.0653, lat=>-68.6102}, suite=>Suite 847, zipcode=>59590-4157, city=>McKenziehaven, street=>Douglas Extension}, name=>Clementine Bauch, id=>3, company=>{name=>Romaguera-Jacobson, catchPhrase=>Face to face bifurcated interface, bs=>e-enable strategic applications}, phone=>1-463-123-4447, email=>[email protected], username=>Samantha, website=>ramiro.info}
    24. username: Samantha
    25. Hi guest
    26. onReceiveSingleUser executed
    27. responseCode: 200
    28. data: {address=>{geo=>{lng=>-164.2990, lat=>29.4572}, suite=>Apt. 692, zipcode=>53919-4257, city=>South Elvis, street=>Hoeger Mall}, name=>Patricia Lebsack, id=>4, company=>{name=>Robel-Corkery, catchPhrase=>Multi-tiered zero tolerance productivity, bs=>transition cutting-edge web services}, phone=>493-170-9623 x156, email=>[email protected], username=>Karianne, website=>kale.biz}
    29. username: Karianne
    30. Hi guest
    31. onReceiveSingleUser executed
    32. responseCode: 200
    33. data: {address=>{geo=>{lng=>62.5342, lat=>-31.8129}, suite=>Suite 351, zipcode=>33263, city=>Roscoeview, street=>Skiles Walks}, name=>Chelsey Dietrich, id=>5, company=>{name=>Keebler LLC, catchPhrase=>User-centric fault-tolerant solution, bs=>revolutionize end-to-end systems}, phone=>(254)954-1289, email=>[email protected], username=>Kamren, website=>demarco.info}
    34. username: Kamren
    35. Hi guest
    36. Received all users