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

Async ordering question (Aaron, Scott, other experts?)

1573875204

Edited 1573875908
GiGs
Pro
Sheet Author
API Scripter
This question is about character sheet functions, but its about general scripting so I'm asking here. I was looking at some versioning workers on the repository, and noticed a pattern that raised a question.  getAttrs(['some stats'], (v) => { // do stuff setAttrs({'stats': values}); // this is pseudocode :) }); setAttrs({version: 1}, () => {versioning()}); With this structure you have getAttrs, with a setAttrs inside it (cal it setA), and another setAttrs following it (call this one setB). With all those functions being asynchronous, will they always work in the order listed? With setA (the nested one) always happen before the outer setAttrs (setB). ? To be clear, I'm wondering if these should be structured something more like this: getAttrs(['some stats'], (v) => { const settings = {}; settings['stats'] = values; settings['version'] = 1;     setAttrs(settings, () => {versioning()}); }); Or do getAttrs and setAttrs always work in sequence, like the first bit of code above?
1573886564
Scott C.
Forum Champion
Sheet Author
API Scripter
Compendium Curator
Heh, not sure I deserve to be called an expert, but here it goes. And I'm sure Aaron will drop by with the actual technical explanation. Because getAttrs is asynchronous and setAttrs is asynchronous, you really can't tell what order they will fire in. Usually, it will be the order listed, but it doesn't have to be. And there's no way to control the order if they're organized like that example; it's all just based on whichever one runs first - the very definition of a race condition. In your example, depending on what versioning() does, this may be perfectly fine. But, I do my versioning a little differently. On mobile, so a code example isn't happening atm, but take a look at the versioning in the Starfinder by Roll20 sheet for an example of what I'm going to describe. On sheet open, I fire a getAttrs. This getAttrs grabs the values of pretty much every attribute on the sheet. Then I look at the version attribute (sheet's old version) and the version variable (sheet's new version). If they're different I work through all the versions between the two, in order of release (this is important). I store any changes to attributes in a setObj and apply them to the object that we got with getAttrs so that my attribute history is updated when I do the update for each version, and I wind up with an object at the end that only contains all the changes so I can do a single setAttrs() call. Note that this method is really just an extension of how I deal with any change on the sheet. If at all possible, you should avoid doing Cascades of getAttrs -> setAttrs -> getAttrs -> repeating as it is extremely time consuming. As a rough example, the same large sheet change (say a level up) using the same logic might take 2 seconds using the Cascade method, but will take only 20ms using a method like I describe above. That's the difference between the user asking why didn't my attack change and them not even noticing all the recalculations.
1573917510
GiGs
Pro
Sheet Author
API Scripter
Scott C. said: Heh, not sure I deserve to be called an expert, but here it goes. And I'm sure Aaron will drop by with the actual technical explanation. Because getAttrs is asynchronous and setAttrs is asynchronous, you really can't tell what order they will fire in. Usually, it will be the order listed, but it doesn't have to be. And there's no way to control the order if they're organized like that example; it's all just based on whichever one runs first - the very definition of a race condition. In your example, depending on what versioning() does, this may be perfectly fine. That confirms what I was thinking. Versioning in the examples I've looked at that use this design pattern always has more getAttrs and setAttrs inside it - its basically a recursive script, that on each recursion, does one set of version changes (with its own getAttrs and setAttrs inside it, and a setattrs beside it). It struck me that if any of those version loops changed the same states, they could work in the wrong order because of asynchronicity and this particular structure. Whereas if they combined the version setting and version changes setAttr into one, they loop into the next recursion inside the setAttrs's callback function, they avoid that danger because that doesnt run until the setAttrs is finished. Your version of handling versioning does seem impressive, but maybe a bit extravagant! Personally I don't mind a bit of a delay during a version change, because it only happens once, on sheet opening, after aversion change. And its easier to compartmentalise the code for each version. I think this might be overstating things a bit: If at all possible, you should avoid doing   Cascades of getAttrs -> setAttrs -> getAttrs -> repeating as it is extremely time consuming.  The normal operation of many sheets will have some of this going on automatically. You change a stat, that triggers getAttrs/setAttrs to recalculate the stat bonus; that then triggers getAttrs/setAttrs to recalculate a handful of skills and other derived stats that that use that stat; that might then trigger another getAttrs/setAttrs change to anything they effect, until the changes cascade out and finally settle. For some really heavy sheets this might be noticeable, but for most it wont. I prefer modularisation like that and taking a small performance hit, though if it took 2 or more seconds everytime you made a change to a sheet that would definitely need changing. I have heard some sheets do wrestle with that problem - once I make sheets big enough to face that problem, I may change my tune :) Another advantage of sheets written this way is anyone can maintain them - someone could take over from me and find the code mostly understandable and make any changes they needed to. I think thats really important for community sheets, and is worth a slight drop in optimisation. Anyway thank you, you did confirm what I thought had ti be the case with those recursive versioning scripts.
1573922893

Edited 1573923012
Scott C.
Forum Champion
Sheet Author
API Scripter
Compendium Curator
Glad to help. But, just to beat a dead horse ;) GiGs said: The normal operation of many sheets will have some of this going on automatically. You change a stat, that triggers getAttrs/setAttrs to recalculate the stat bonus; that then triggers getAttrs/setAttrs to recalculate a handful of skills and other derived stats that that use that stat; that might then trigger another getAttrs/setAttrs change to anything they effect, until the changes cascade out and finally settle. Yes, this is how many sheets are coded. However, it's not a good idea even on small compact sheets for a few reasons: Performance: While it is true this won't be noticeable on very small sheets, doing cascades where you have a change that then propagates to other listeners is somewhere between 10x and a 100x slower than doing a single function that handles all the changes. Also, note that this decrease in speed is cumulative if you have multiple cascades. Reliability: Because of the race conditions that you noticed in those versioning methods, relying on these cascades can also cause unintended results if the multiple change events wind up firing in a different order. Ease of maintenance: Switching to a method where any attribute you might need or might need to change is grabbed with a single getAttrs() (and all are set with a single setAttrs() ) allows you to mostly ignore the vagaries of asynchronous coding, which actually makes your code easier to understand and maintain. Practice Makes perfect: And finally, getting used to doing a new method is easiest on smaller sheets. Then, when you do tackle a large sheet project (or any project really) those good coding practices are already ingrained
1573923487

Edited 1573923641
GiGs
Pro
Sheet Author
API Scripter
While you make good points, I notice that most of the official roll20 sheets use the method I describe (including the flagship D&D 5e sheet). There's a difference between perfect and good enough, and I'm happy with good enough. The method I describe (change stat -> cascades to stat bonus -> cascades to skill) simply cannot fail to race conditions, because each one triggers the next, and they are all one way. It's true if you had a really complex arrangement of attributes where changes can cascade in different directions, race conditions could be an issue - but so could infinite loops, and so yes, you'd need to avoid cascades. But I've never encountered a situation of normal use of a character sheet where this would be an issue (that doesnt mean they dont exist, of course). Versioning is different, because different versions can change the same attributes, and they can be overlapping changes that even change which attributes exist, so its much more important to get the sequencing correct.