0

While putting to practice what I've learned so far about ES2015 with Babel, specifically about WeakMaps, I came upon a problem I don't know why it's not working.

I have a WeakMap defined to house data coming from an AJAX call that's only triggered if the value of said WeakMap is undefined.

This is what I came up to:

class User {

    constructor( id ) {

        id = Number( id );

        if( id <= 0 || isNaN( id ) ) {
            throw new TypeError( 'Invalid User ID' );
        }

        _id.set( this, id );
    }

    getID() {
        return _id.get( this );
    }

    getData() {

        let _this = this;

        if( _data.get( _this ) === undefined ) {

            _this.loadData().done( function( data ) {

                // JSON is indeed successfully loaded

                console.log( data );

                _data.set( _this, data );

                // WeakMap is indeed set correctly

                console.log( _data.get( _this ) );
            });
        }

        // But here it's undefined again!

        console.log( _data.get( _this ) );

        return _data.get( _this );
    }

    loadData() {

        return $.get({
            url: '/users/' + _id.get( this, data ),
        });
    }
}

let _id   = new WeakMap;
let _data = new WeakMap;

// ---------------

var user = new User( 1 );

console.log( user.getID(), user.getData() ); // 1 undefined

As far as i know, I am setting the WeakMap data correctly, as the User ID is being set and can be retrieved, but the User Data coming from AJAX, although is indeed being set inside the jQuery.done() can't be accessed outside of it.

what i'm doing wrong?

user5613506
  • 286
  • 1
  • 16
  • Your `getData` returns synchronously, before the function passed to `done` has finished running. You'd want to make `getData` return a promise too. This is an extremely common gotcha in JS code, and happens with or without your WeakMap. – loganfsmyth Jan 25 '17 at 17:15
  • Maybe it's my PHP experience talking but in my mind the purpose of `getData()` was to be a *getter* for the WeakMap, populating it if it's hasn't been done yet. If I make it return a Promise as well (which I'm not exactly sure how yet), I would have to resolve it (with... `deferred.done()`, maybe?) wherever and whenever I call it, wouldn't I? Can't I avoid this without setting `async`to `false` (only way that worked so far)? – user5613506 Jan 25 '17 at 21:26
  • The same way that `loadData` returns a promise, `getData` should return one. You could have the WeakMap store the promise itself, if you want to cache things. – loganfsmyth Jan 25 '17 at 22:34
  • I'm sorry, maybe this make sense for you, but not for me. I've tried several examples and none of then worked. – user5613506 Jan 26 '17 at 11:44

2 Answers2

0

I don't understand JavaScript to the point saying this is the right solution or not but I've searched A LOT, reading countless questions here in Stack Overflow with immense answers that fails to put things ins simple ways so, for anyone interested:

class User {

    constructor( id ) {

        id = Number( id );

        if( id <= 0 || isNaN( id ) ) {
            throw new TypeError( 'Invalid User ID' );
        }

        _id.set( this, id );
    }

    getID() {
        return _id.get( this );
    }

    getData() {

        if( _data.get( this ) === undefined ) {
            _data.set( this, this.loadData() );
        }

        return _data.get( this );
    }

    loadData() {
        return $.getJSON( CARD_LIST + '/player/' + _id.get( this ) );
    }
}

let _data = new WeakMap;

// ---------------

var user = new User( 1 );

user.getData().done( function( data ) {

    console.log( data );
})

It's not what I had in mind initially and I don't have knowledge to explain the "whys" but, at least this is a palpable working example that I humbly hope that will helps someone else who's trying to extract info from extremely long answers and/or unhelpful/unguided comments.

user5613506
  • 286
  • 1
  • 16
  • This looks like it stores `jqXHR` objects (which represent the state of the Ajax fetch performed by `$.getJSON`. Did you want your WeakMap to be populated by jqXHR objects...? – apsillers Jan 26 '17 at 12:19
  • No, initially i wanted to have only the JSON returned, but I couldn't any other way other than setting `async: false` – user5613506 Jan 26 '17 at 12:22
0

See How do I return the response from an asynchronous call? for a (lengthy) primer of how to use the result of an asynchronous operation like Ajax.

The short answer is: you cannot use an asynchronously-fetched result if you're synchronously returning a value. Your getData function returns before any Ajax call resolves, so the synchronous return value of getData cannot depend upon any asynchronously-fetched value. This isn't even a matter of "waiting" long enough: the basic principle of JavaScript event handling is that the current function stack (i.e., the current function, and the function that directly called that function, etc.) must resolve entirely before any new events are handled. Handling an asynchronous Ajax result is a new event, so your done handler necessarily will not run until well after your getData function execution is ancient history.

The two general ways to solve your issue are:

  1. Make the Ajax call synchronous

  2. Make the Ajax-fetched value accessible asynchronously outside of getData

You can do #2 by either passing a callback into getData, like

getData(funnction(data) { console.log("If this is running, we finally got the data", data); }

function getData(callback) {
    _this.loadData.done(function(data) {
        _data.set(this, data);
        // etc. etc.
        callback(_data.get( _this ));
    });
}

So, now the getData is itself asynchronus, does this force you to rewrite any use of `getData you already have, and cause propagation of asynchronous patterns all over your code? Yes, it does.

If you want to use return a promises instead, you could do

getData().then(function(data) {
    console.log("Got the data", data);
});

function getData() {
    return _this.loadData().then(function(data) {
        _data.set(this, data);
        // etc. etc.
        return _data.get( _this );
    });
}

this works because promises allowing chaining then calls. This simply chains then inside and outside the get data function: one then callback is queued inside getData, and the next is queued outside getData. The return value of the first callback is used as the argument of the second. The return value of then is the original promise, so I've simply used the form return mypromise.then(...), which returns the same object as if I'd just done return mypromise (but obviously I haven't set up a callback in that case).

Community
  • 1
  • 1
apsillers
  • 112,806
  • 17
  • 235
  • 239