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

A first time only listener for the sheetworker

1714014231

Edited 1714023341
Marco M.
KS Backer
Sheet Author
API Scripter
Compendium Curator
I came up with an idea for a listener that only works once, no matter what the event is (it does not require a sheet opened; any sheet worker event can trigger it. I never saw anyone using something similar here, or better yet, I couldn't find anything similar on the forum or the wiki. I have a character sheet that requires a lot of initialization every time it is opened. I was curious to see if I could generate an object containing all the information for my character and use that to set attributes around the sheet instead of using getAttrs or getSectionIDs. Even for unrealistic sheets, I couldn't make the object bigger than 0.0019Mb per character. The access and removal time would also be O(1). To avoid any problems if the player does not open their sheet as their first action (let say if they use a macro in chat calling an ability on the sheet) I created a general onEvent listener. // the flag const sett = { isFirst : true }; // some code to run only once containing initial settings const coderunonopen = () => {         ... }; // the meat and potato taking over the event function const onEvent = (event,callback) => { on(event, (eventInfo) => { const isFirst=sett.isFirst; if (isFirst){ coderunonopen(); sett.isFirst=false; } callback(eventInfo); }); } I'm still not sure what problem it can create, but so far, it seems to work pretty well for everything from compendium drop to change in attributes to macro buttons **UPDATE**: with the callback at the end of onEvent there is no need to use async and await in the function. On top of what Scott correctly said in 1. It served no purpose in my function, and I removed it .
1714016333

Edited 1714016687
Scott C.
Forum Champion
Sheet Author
API Scripter
Compendium Curator
There's a couple problems here. You are using async/await. In character sheets, async/await will not work with the sole exception of using it to await startRoll. Any other usage will break the link between the thread and the active character sheet A minor issue that is easily fixed is that you'd also need to add the Jquery stub listener so that any events triggering that also use this setup. The properties of event usually  contain the newValue, but there are a few things that can cause them to not contain the appropriate newValue/previousValue. The big one that I remember here is that API caused changes to attributes don't, but I belive there's a few others as well. It's been so long since I relied on the event properties that I've forgotten all the ways it could get screwed up. This means you have to do a getAttrs anyways regardless of what you are doing. Similarly to problem #2, the API can cause multiple changes at once. This will cause a race condition to occur. Depending on when you set your isFirst  variable, you will either wind up with the object containing the character information being out of date (empty if this is the first run) (which is bad), or the initialization will run multiple times. The multiple runs might not seem bad, but if any of those are setting variables, you wind up with each "thread" being desync'd and seeing different data. The global variables are global for the client, so this data has to be indexed by some sort of character sheet unique identifier (like the character ID). However, then you still need to do a getAttrs for every event so that you know which set of data to grab for any given event. Issues #1 and #2 are just code setup issues that are easy to fix. Unfortunately the other issues are more serious. Issue #4 is probably the biggest issue that could cause errors that would be very difficult to track down because they'd manifest differently each time. Issues #3 and #5 mean that you're still having to do a getAttrs with each event anyways, so you aren't actually saving that much time; roughly 0ms per event if the event doesn't need repeating section information, and even if it does require repeating section information you're only saving ~10ms per repeating section you need information from. So you're adding some code complexity for no real performance gains (definitely not gains that a human can notice). The big time sink in sheetworkers is setAttrs which takes ~100ms per call, so the best way to make efficient sheetworkers is to make as few setAttrs calls as possible; e.g. set all the attributes from a given change at once and DON'T do cascading events where you rely on the event detection to propagate a change through all the things that depend on it. To make the long story short, you're on the right track with wanting to reduce the calls to the network, but the way to actually improve performance is to have every event get all attribute data for the sheet and then setup a pseudo event cascade within that thread so that all your changes are combined into a single setAttrs. You wind up with a few tens of milliseconds of extra time for any given event, but the overall performance for complex interactions is several hundred milliseconds faster. This is what the  K-scaffold  does to ensure that you can write simple sheetworker functions without needing to worry about the event handling. For those interested, a more detailed breakdown of the code efficiency of cascading events vs. single setAttrs is below. Here's an example of this in human readable logic instead of code. The setup for this demonstration is: attribute1, attribute2, and attribute3 are user editable dependent1 = attribute1/attribute2 dependent2 = dependent1 + attribute3 Cascading events method: When attribute1 or attribute2 are changed by the user, dependent1 is set by the sheetworker. The listener sees dependent1 change and fires a new event to calculate and set dependent2. The total time for this pathway is going to be about 10ms for each getAttrs (20ms total) and about 100ms for each setAttrs (200ms total) for a total of ~220ms. Single setAttrs method: When attribute1 or attribute2 is changed by the user, a function is fired which sets dependent1 and calculates dependent2. When attribute3 is changed by the user a function is fired which sets dependent2. The total time for this pathway, regardless of which attribute is edited by the user will be ~10ms for the getAttrs and ~100ms for the setAttrs for a total of 110ms. Now, the difference between these two pathways wouldn't be detectable by a human user, but imagine if you scale this up to a pathway that has 5, 6, 7, or more dependent attributes we can see a huge difference. Using the cascading event method, each of those is going to add ~110ms to the total resolution time. Around ~500ms a user will start to notice the delay, and at ~1 second the delay will be long enough that the user can reliably cause another user caused change while the calculations of the first are still resolving creating a race condition that can cause calculations to be unreliable. With the single setAttrs method, the resolution time for these dependent attributes will always be ~110ms whether you're doing a chain of 2 dependents or 10,000 dependents.
1714020902
GiGs
Pro
Sheet Author
API Scripter
Pay attention to Scott, he knows of what he speaks :) I'll ramble a bit but then I have an important question. I do do somethings differently than as described here, and in any random question would probably give different advice (especially regarding cascades, explanation below), so i should specify that I agree with everythign said here. I do sometimes use the event properties (mainly because of my different stance on cascades, I sometimes require them), but I agree that I try to avoid using newValue and previousValue. I think (think, am not 100% sure) that the problem with them and the API was a temporary bug and has been fixed, but I still try to avoid them - its the other event properties I sometimes use. Regarding cascades: I think that these are acceptable in a lot of situations. You do sometimes have to be careful, but as long as you keep the number of getAttrs and setAttrs calls to 1 in each sheet worker, and use sheet:opened only when absolutely necessary, then cascades are very rarely an issue. (That said, Scott works with sheets much heavier than the ones I usually use, so he may see more issues than I do.) Important Question Marco, is there a reason you need to do this? Usually, a sheet:opened worker with a version attribute (and check to see if version has changed) has always met my needs and is pretty simople (and handles situations when you wabnt that worker to fire a second or third time, to track changes in a sheet's version). is there some situation where this approach is inadequate?
1714022348

Edited 1714022459
Marco M.
KS Backer
Sheet Author
API Scripter
Compendium Curator
Five big reasons: 1. I need to know the old values of some functions in listeners that can trigger different events. In this case, the previous might not refer to the same attribute of the new value. In practice, that means that I either make the HTML/Pug part of the sheet unmaintainable by storing and attribute _old for every sheet  OR I use an object that has to be recreated at least once every time I start a game session (I could find a way to save a variable from session to session in JS). Since this goes beyond versioning, I need to find a general way for the object not to be recreated every time I open the sheet, and this one looked elegant and simple. 2. Playing with complex rolls and JQuery, sometimes I need to be sure that the value I pass to some button in the repeating sections has been assigned correctly. Hence, an initialization when you start the game is always useful and safe (and it goes beyond versioning).  3. I reopen my sheet a lot, so I have to see all the logs off operations I don't care about in particular when I am refactoring a sheet 4. With reacting rolls, I am minimizing how often people even open the sheet (thanks, CPR), so there could be a breaking update requiring the sheet to be opened at least once before I press a macro button in the chat. Still, the player didn't do it (onEvent check the first for any event, not just sheet:opened).  The idea is to have the equivalent of a ready but without the API 5. The choice is to get all the attributes and repeating sections at once in byzantine nested callbacks or use the K-scaffold. This makes it hard to have reusable one-purpose functions where you operate over a set of attributes due to the asynchronous nature of getAttrs and getSectionIDs... In short, you cannot build your code like a Lego; you must build it like a matrioska. Since the overhead in terms of the memory of creating an object with all the information of a chargesheet is negligible, even for big repeating sections and expansive campaigns, I thought it could be a good idea Note: the API integration was my biggest concern, and Scott helped me to understand better the limitations of my approach
1714167279
GiGs
Pro
Sheet Author
API Scripter
Thank you for the explanation. I can see sheet:opened might sometimes be inadequate (when you know the sheet has changed, but someone hasn't opened their sheet yet). I worry you might be trying to do something that isn't supported in roll20 - you do have to work within its limitations. Some of what you're trying to do are things I just wouldn't attempt on roll20 (though that is partly because of previous bugs that went uncorrected for a long time, and partly because of how clunky it is).
1714211925
Marco M.
KS Backer
Sheet Author
API Scripter
Compendium Curator
I love to push roll20 limits :)  Since the last time I worked on sheets, they have opened a lot, and I was really impressed. So far, the changes seem to work, but I am developing both classic JS and this onEvent form, so if one doesn't work after I release a sheet, I can fold to the safe one.