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

Getting Notes from Handout - Promise vs. Async

1593631221
timmaugh
Pro
API Scripter
Let me lead off with the question, then present the context (you tell me if I'm even asking the right question)... Do I need to use a promise to read the notes property of a handout object? And, if so, do I need to wrap all subsequent code in a .then() block? In other words, does getting information from the notes of a handout suddenly require me to turn all of my code asynchronous... or can I handle this in a callback and call it a day? Here's what I'm trying to do... for a script, there could be a game-wide configuration handout and/or a speaker-level configuration handout. The script should look to see if there is a speaker-level configuration (the most specific), and if it doesn't see that it should look for a game-wide configuration, and if it doesn't find that, it should read its own default internal configuration. In game, this might mean that the GM has set up a game-wide look for certain interactions, but the players might have set up specific configurations for their characters. I can get the notes out of the config handout, but the way I have it written right now (with a promise) makes it wait to resolve until the rest of the active stack gets resolved. So, using the revealing module pattern, my script runs handleInput (not yet wrapped in a then() block): const handleInput = (msg_orig) => { if (msg_orig.type !=="api") return; let apicatch = "", xr = "xr", ia = "ia", vw = "vw"; if (new RegExp(/^!xray\s/).exec(msg_orig.content.toLowerCase()) !== null) apicatch = xr; else if (new RegExp(/^!insertarg(\s|s\s)/).exec(msg_orig.content.toLowerCase()) !== null) apicatch = ia; if (apicatch === "") return; let args = msg_orig.content.split(/\s--/) .slice(1)     // get rid of api handle .map(splitArgs)     // split each arg (foo:bar becomes [foo, bar]) .map(joinVals); // if the value included a colon (the delimiter), join the parts that were inadvertently separated const abil = args.shift();     // assign the first arg to abil let cmdline = ""; let theSpeaker = getTheSpeaker(msg_orig); let configObj = getConfig(theSpeaker); log(`===== CONFIG ===== `); log(configObj);         /* other stuff happens */         sendChat(...........)         return;     }; The getConfig() function looks like this: const getConfig = (theSpeaker) => { let cfgObj = { table: menutable, row: menurow, bg: "#ff9747", css: "" }; let cfgho = findObjs({ type: "handout", name: `IAConfig-${theSpeaker.localname}` })[0] || // get the config for this player/character, if present findObjs({ type: "handout", name: "IAConfig-Global" })[0] || // if no specific config, look for global { get: () => { return ""; } }; // if no config exists, return an object with a single method let cfgtext = ""; let ConfigNotes = new Promise((resolve, reject) => { cfgho.get('notes', (notes) => { resolve(notes); }); }); ConfigNotes.then((result) => { cfgtext = result; // get table components into the configObj let m = new RegExp(/(<!--\sBEGIN\sTABLE\s-->.*?<!--\sEND\sTABLE\s-->).*?(<!--\sBEGIN\sROW\s-->.*?<!--\sEND\sROW\s-->)/, 'gmi').exec(cfgtext); // group 1: table delimited by table tags from tablerow // group 2: row delimited by row tags from tablerow if (m) Object.assign(cfgObj, { table: m[1], row: m[2] }); // get config components into the configObj m = new RegExp(/(<!--\sBEGIN\sCONFIG\s-->.*?<!--\sEND\sCONFIG\s-->)/, 'gmi').exec(cfgtext); // group 1: config section delimted by config tags if (m) { let settings = ["css", "bg"], // settings available in the config section sdata; // to hold the data from each setting read out of the config settings.map((s) => { sdata = new RegExp(`/^ --${s}#(.*?) \n/`, 'gmis').exec(m[1]); //group 1: user input for the setting from --setting#user input if (sdata) cfgObj[s] = sdata[1]; }); }             log(`During Promise: ${result}`); return cfgObj; }); }; This plan doesn't work. With things like this, my Promise doesn't resolve until after all of the synchronous code finishes. Even though the log line during the promise does have the correct data in it (the notes from the handout object), it doesn't run until after the handleInput has blown on by. My log looks like this: "===== CONFIG ===== "                   undefined                                                     // <=== log(configObj) "During Promise: ..... /* data from notes */ .....            // <=== log() during promise Does this mean that everything downstream of the getConfig() call has to be wrapped in a then() block? Or can the waiting on the asynchronous get('notes') happen in the callback? (I think I tried that but didn't get it to work.)
1593666693
The Aaron
Roll20 Production Team
API Scripter
You can use a callback.  If you use a promise (or async/await), it is infectious and all the code following it must use them until the point where you don't care about doing anything afterwards.  Personally, I would reorganize the code to load all the configs into a variable at "ready", abs then reference that variable instead. It will be more efficient and you get rid of the complexity of the async calls later. You can add some on("change:handout:notes",...) calls to rebuild the cached config data if it gets changed.  Side note, you can replace this: if (new RegExp(/^!xray\s/).exec(msg_orig.content.toLowerCase()) !== null) With this: if (/^!xray\s/i.test(msg_orig.content)) Which is probably more efficient, but definitely more readable.  I only explicitly construct RegExp objects if I need to build them dynamically, and then I build them either once at "ready", or as a separate variable to make the checks cleaner  
1593695442
timmaugh
Pro
API Scripter
That is... such a more elegant solution. I had managed to kludge it together by breaking the pre-async stuff (and the get('notes') callback) into its own formula that listened for the chat event, then handled the processing of the notes in the callback before (still in the callback) finally calling a different function that was basically all of the old synchronous stuff that would have followed the get(). It let me imagine the code as still synchronous without having to declare it async... it was just time-displaced. =) I knew it wouldn't work for a more complex implementation, and it was a lot of overhead for script calls that might not even need it. Using the "ready" event and "change:handout:notes" solves both those problems. Thanks for the catch on the regex, too. For that sort of inline, immediately-invoked regex (iiirx?), there is no way to pass flags, correct? So flags would be another reason to use the new RegExp() statement? And when you say you build them at "ready", are you putting them in the on(ready...) portion, or in the object closure? Or is there even a difference?
1593696130
The Aaron
Roll20 Production Team
API Scripter
You can pass flags on the end: if (/^!xray\s/ i .test(msg_orig.content)) For where to build the dynamic RegExp, I just do it at the most persistent spot I can get away with.  In the case of something like available status markers, I'd do that once when "ready" happened and use it all over.  In the case of something like a list of possible weapon names from the arguments to a chat command, I'd build it as soon as I had the arguments.
1593698140
timmaugh
Pro
API Scripter
I did not know that (flags). Thank you!
1593698642
The Aaron
Roll20 Production Team
API Scripter
No problem. =D