Roll20 uses cookies to improve your experience on our site. Cookies enable you to enjoy certain features, social sharing functionality, and tailor message and display ads to your interests on our site and others. They also help us understand how our site is being used. By continuing to use our site, you consent to our use of cookies. Update your cookie preferences .
×
Create a free account

Problems inside GM Notes callback function

June 30 (6 years ago)

Edited June 30 (6 years ago)

I have a function that should return either a token object or a character object depending on which one has specific text in the GM Notes section. The problem is that nothing is happening inside the callback function to test for the text. I've tried everything from setting variables declared outside the callback to simply using a return inside the callback. Here's some pseudo code to illustrate:

myFunction (id) {
var obj;
var token = getObj('graphic', id);
if (token && token.get('represents') != '') {
token.get('gmnotes', function(notes) {
if (notes = 'my test') obj = token;
});
if (!obj) {
var char = getObj('character', token.get('represents'));
if (char) {
char.get('gmnotes', function(notes) {
if (notes = 'my test') obj = char;
});
}
}
}
return obj;
}

This function will never return anything but the undefined obj, regardless of whether the GM Notes contain the text I'm testing for. (I expect it to return undefined if neither passes the test.)

Then I tried to just return the already created object inside the callback. It met with the same results:

myFunction (id) {
var token = getObj('graphic', id);
if (token && token.get('represents') != '') {
token.get('gmnotes', function(notes) {
if (notes = 'my test') return token;
});
var char = getObj('character', token.get('represents'));
if (char) {
char.get('gmnotes', function(notes) {
if (notes = 'my test') return char;
});
}
} else {
return null;
}
}

Vigorous logging shows that I am definitely getting those objects, it's just that it all goes silent once inside that callback for the GM Notes. That is, logging happens inside the callbacks, but nothing else.

Is there something else I need to do here? Or what am I missing? Surely this is possible somehow.

July 01 (6 years ago)
The Aaron
Roll20 Production Team
API Scripter

What you're missing is how asynchronous functions work. The get for gmnotes is asynchronous, which means the thread of execution continues through the rest of the calling code while the asynchronous get function is waiting to be resolved.  You cannot return data from an asynchronous function it the traditional sense, and setting variables in the calling closure won't happen when you think it should.

When dealing with asynchronous functions, you always must "pay it forward" in a sense, where there is no code after the asynchronous function call, and the asynchronous function calls back into your code when it is done.  I'll try to find an example later when I'm not on my phone, but you can google for "javascript callback hell" or "javascript asynchronous functions" or "javascript promises" if you want to read up on it.



The Aaron said:

What you're missing is how asynchronous functions work. The get for gmnotes is asynchronous, which means the thread of execution continues through the rest of the calling code while the asynchronous get function is waiting to be resolved.  You cannot return data from an asynchronous function it the traditional sense, and setting variables in the calling closure won't happen when you think it should.

When dealing with asynchronous functions, you always must "pay it forward" in a sense, where there is no code after the asynchronous function call, and the asynchronous function calls back into your code when it is done.  I'll try to find an example later when I'm not on my phone, but you can google for "javascript callback hell" or "javascript asynchronous functions" or "javascript promises" if you want to read up on it.

I did do some research on callback functions, and I kind of understand in a general sense. But none of it has helped me figure out how to get this to work. I will wait patiently for your example. Thanks!

July 02 (6 years ago)
The Aaron
Roll20 Production Team
API Scripter

Here's an excerpt from the Message of the Day script.  It gets called whenever a handout is changed:

    const handleNoteChange = (obj) => {
        if(obj.id === motdNoteId) {
            log('MotD Note changed.');
/* 1 */     setTimeout(()=>{
/* 2 */         obj.get('notes',(notes) => {
/* 3 */             obj.get('gmnotes',(gmNotes)=>{
                        loadMotDNote(notes, gmNotes, obj.get('avatar'));
                    });
                });
            },loginSendDelay);
        }
    };
There are three asynchronous calls going on here. 

The first one is the call to setTimeout(func, ms), which will wait the specified number of milliseconds, then call the supplied function.  I believe I added this because handouts are a bit weird, and if I tried to fetch them immediately when I was told they had been changed, I would get back the old source.  It's likely a race condition because of how handout contents are stored, but delaying 10 seconds to read it means I get back the changed contents.  If this was not asynchronous, and there was instead some way to have the API wait 10 seconds before continuing, nothing else in the API could do anything.  (Javascript basically uses cooperative multitasking for high performance in a single threaded environment.)

The second one is a more familiar fetching of the notes section of the handout using .get('notes', func).  Because Notes are stored as a blob field outside of Firebase, they take a bit more effort to fetch.  To prevent that from hanging the API as in the above setTimeout() discussion, an asynchronous .get() method was introduced.  When the fetch for the notes completes in whatever database layer is being used (I think its Postgres), the Javascript Virtual Machine will cause the supplied function to be called with the notes contents as its argument.

The third one is basically identical to the second, save that it is this time fetching the GMNotes field.  Once that asynchronous call is finished, it finally calls the loadMotDNote() function which stores all the data from both those fields (and some other stuff) so that it can show the Message of the Day easily without going through all that again.


I just remembered I wrote up a bit about this on the wiki a few years ago, this might also help a bit: https://wiki.roll20.net/API:Use_Guide#A_Treatise_on_Asynchronous_Functions

Let me know if you want me to delve deeper, I'll talk programming till I'm blue in the face. =D

I would love to delve deeper on this! I will message you with more specifics on what I'm trying to do. Thanks!

July 02 (6 years ago)
GiGs
Pro
Sheet Author
API Scripter

If you could keep the discussion public, that would be great, since I'm sure there are others (me), mentioning no names (me), that might be interested in how to solve async related problems.

GiGs said:

If you could keep the discussion public, that would be great, since I'm sure there are others (me), mentioning no names (me), that might be interested in how to solve async related problems.

I will definitely post results here, but my solution may involve bypassing the asynchronous dilemma altogether. Also, everyone else is definitely encouraged to post their async solutions and workarounds here for sure!

July 02 (6 years ago)

Edited July 02 (6 years ago)
Scott C.
Forum Champion
Sheet Author
API Scripter
Compendium Curator

Well, here's how I'd handle it, using the async/await and promise features of modern JS; also adding some fixes for some coding errors:

let myFunction = async function(id) {
var obj;
var token = getObj('graphic', id);
if (token && token.get('represents') !== '') {//see the full discussion of this in the next comment, but essentially always use the type comparison, never use the not type specific comparison                 obj = await new Promise((resolve,reject)=>{
    token.get('gmnotes', function(notes) {
if (notes === 'my test'){//The use of just the = was causing notes to be assigned the value of 'my test' rather than comparing its value to 'my test'. Also, always use === to compare; never use ==. == can cause some very strange behavior for some comparisons.                             resolve(token);                         }else{                             let char = getObj('character',token.get('represents'));                             if(char){                                 char.get('gmnotes',(cnotes)=>{                                     if(notes==='my test'){                                         resolve(char);                                     }else{                                         reject('no valid token');                                     }                                 });                             }else{                                 reject('no valid token');                             }                         }
    });                 });
}
return obj;
}

You would then use a similar async/await function in the function looking for the obj value:

let objUser = async function(id){
    let realObj = await myFunction(id);
    log(realObj);
}

EDIT: Note that I would probably actually change the myFunction so that the promise was done at obj's declaration, but the above should serve as a good example of using async/await and promises.

Thanks, Scott! Yes, my pseudo code was hastily written so my comparison turned into an assignment.Oops!

I will definitely give that approach a shot and see how it turns out. What would be the difference between your example and doing the promise at the obj's declaration as you suggest in the edit?

July 03 (6 years ago)

Edited July 03 (6 years ago)
Scott C.
Forum Champion
Sheet Author
API Scripter
Compendium Curator

Mostly just style, although I have a vague memory of mixing the possible return values of functions between promises and regular values causing issues. here's what the code would look like doing it at the declaration:

let myFunction = function(id) {
    return new Promise((resolve,reject)=>{
        var token = getObj('graphic', id);
        if (token && token.get('represents') !== '') {//see the full discussion of this in the next comment, but essentially always use the type comparison, never use the not type specific comparison
            token.get('gmnotes', (notes)=>{
                if (notes === 'my test'){//The use of just the = was causing notes to be assigned the value of 'my test' rather than comparing its value to 'my test'. Also, always use === to compare; never use ==. == can cause some very strange behavior for some comparisons.
                    resolve(token);
                }else{
                    let char = getObj('character',token.get('represents'));
                    if(char){
                        char.get('gmnotes',(cnotes)=>{
                            if(notes==='my test'){
                                resolve(char);
                            }else{
                                reject('no valid token');
                            }
                        });
                    }else{
                        reject('no valid token');
                    }
                }
            });
        }else{
            reject('Not a valid selection');
        }
    });
}