In JS query, `setTimeout` does not run according to the specified timeout

Goal: call setTimeout reliably from JS query

Steps:

I have a simple page with a button and a text component.

After I click on the button I am trying to reliably write to that text component every second, 5 times.

I am only getting updates if I manually click in the UI. If I don't, the setTimeout callback runs every 10 seconds or so.

I am on the latest version of Chrome (123.0.6312.123)

Details:

Code for runCommand JS query:

let logs = ""
function appendToLog(message) {
  logs += message + "\n"
  logsContent.setValue(logs)
}

const sleep = t => new Promise(r => setTimeout(f, t))

const startTime = Date.now()
appendToLog("Start");

let i = 0;
while (i < 5) {
  i += 1;

  await sleep(1000)

  appendToLog(`Running for ${i} seconds: ${Date.now() - startTime}`)
}

Screenshots:
Screenshot 2024-04-15 at 17.54.35

App json export:

{
"uuid": "5fcc24be-fb82-11ee-9042-d7bff1fd5ec2",
"page": {
"id": 263100974,
"data": {
"appState": "["~#iR",["^ ","n","appTemplate","v",["^ ","isFetching",false,"plugins",["~#iOM",["$main",["^0",["^ ","n","pluginTemplate","v",["^ ","id","$main","uuid",null,"type","frame","subtype","Frame","namespace",null,"resourceName",null,"resourceDisplayName",null,"template",["^3",["type","main","sticky",false,"isHiddenOnDesktop",false,"isHiddenOnMobile",false,"_disclosedFields",[],"paddingType","normal","padding","8px 12px"]],"style",["^3",[]],"position2",null,"mobilePosition2",null,"mobileAppPosition",null,"tabIndex",null,"container","","createdAt","~m1680561666259","updatedAt","~m1713224842303","folder","","screen",null]]],"runCommand",["^0",["^ ","n","pluginTemplate","v",["^ ","id","runCommand","^4",null,"^5","datasource","^6","JavascriptQuery","^7",null,"^8","JavascriptQuery","^9",null,"^:",["^3",["queryRefreshTime","","allowedGroupIds",["~#iL",[]],"streamResponse",false,"lastReceivedFromResourceAt",null,"queryDisabledMessage","","servedFromCache",false,"offlineUserQueryInputs","","successMessage","","queryDisabled","","playgroundQuerySaveId","latest","workflowParams",null,"resourceNameOverride","","runWhenModelUpdates",false,"workflowRunId",null,"showFailureToaster",true,"query","let logs = \"\"\nfunction appendToLog(message) {\n logs += message + \"\\n\"\n logsContent.setValue(logs)\n}\n\nconst sleep = m => new Promise(r => setTimeout(r, m))\n\nconst startTime = Date.now()\nappendToLog(\"Start\");\n\nlet i = 0;\nwhile (i < 5) {\n i += 1;\n\n await sleep(1000)\n\n appendToLog(Running for ${i} seconds: ${Date.now() - startTime})\n}","playgroundQueryUuid","","playgroundQueryId",null,"error",null,"workflowRunBodyType","raw","privateParams",["^E",[]],"workflowBlockUuid",null,"runWhenPageLoadsDelay","","data",null,"importedQueryInputs",["^3",[]],"_additionalScope",["^E",[]],"isImported",false,"showSuccessToaster",false,"cacheKeyTtl","","requestSentTimestamp",null,"metadata",null,"workflowActionType",null,"queryRunTime",null,"changesetObject","","errorTransformer","// The variable 'data' allows you to reference the request's data in the transformer. \n// example: return data.find(element => element.isError)\nreturn data.error","finished",null,"confirmationMessage",null,"isFetching",false,"changeset","","rawData",null,"queryTriggerDelay","0","resourceTypeOverride",null,"watchedParams",["^E",[]],"enableErrorTransformer",false,"showLatestVersionUpdatedWarning",false,"timestamp",0,"importedQueryDefaults",["^3",[]],"enableTransformer",false,"showUpdateSetValueDynamicallyToggle",true,"overrideOrgCacheForUserCache",false,"runWhenPageLoads",false,"transformer","// Query results are available as the data variable\nreturn data","events",["^E",[]],"queryTimeout","10000","workflowId",null,"requireConfirmation",false,"queryFailureConditions","","changesetIsObject",false,"enableCaching",false,"allowedGroups",["^E",[]],"offlineQueryType","None","queryThrottleTime","750","updateSetValueDynamically",false,"notificationDuration",4.5]],"^;",null,"^<",null,"^=",null,"^>",null,"^?",null,"^@","","^A","~m1713224858822","^B","~m1713224928948","^C","","^D",null]]],"logsHeader",["^0",["^ ","n","pluginTemplate","v",["^ ","id","logsHeader","^4","3d0c66f5-209f-4d11-a6e8-cfd23f8d5205","^5","widget","^6","TextWidget2","^7",null,"^8",null,"^9",null,"^:",["^3",["heightType","auto","horizontalAlign","left","hidden",false,"imageWidth","fit","margin","4px 8px","showInEditor",false,"verticalAlign","center","tooltipText","","value","Logs","disableMarkdown",false,"overflowType","scroll","maintainSpaceWhenHidden",false]],"^;",["^3",[]],"^<",["^0",["^ ","n","position2","v",["^ ","^5","grid","^@","","rowGroup","body","subcontainer","","row",1.4,"col",0,"height",0.6,"width",3,"tabNum",0,"stackPosition",null]]],"^=",null,"^>",null,"^?",null,"^@","","^A","~m1713224902875","^B","~m1713224908423","^C","","^D",null]]],"logsContent",["^0",["^ ","n","pluginTemplate","v",["^ ","id","logsContent","^4","7c217e24-5d48-468a-b834-f6289ec6f4c4","^5","widget","^6","TextWidget2","^7",null,"^8",null,"^9",null,"^:",["^3",["heightType","auto","horizontalAlign","left","hidden",false,"imageWidth","fit","margin","4px 8px","showInEditor",false,"verticalAlign","center","tooltipText","","value","","disableMarkdown",false,"overflowType","scroll","maintainSpaceWhenHidden",false]],"^;",["^3",[]],"^<",["^0",["^ ","n","position2","v",["^ ","^5","grid","^@","","^F","body","^G","","row",1.9999999999999998,"col",0,"^H",0.6,"^I",3,"^J",0,"^K",null]]],"^=",null,"^>",null,"^?",null,"^@","","^A","~m1713224912429","^B","~m1713224915036","^C","","^D",null]]],"button1",["^0",["^ ","n","pluginTemplate","v",["^ ","id","button1","^4","13a2446a-0162-4a64-a378-8b616c23e52d","^5","widget","^6","ButtonWidget2","^7",null,"^8",null,"^9",null,"^:",["^3",["heightType","fixed","horizontalAlign","stretch","clickable",false,"iconAfter","","submitTargetId",null,"hidden",false,"text","Start","margin","4px 8px","showInEditor",false,"tooltipText","","styleVariant","solid","submit",false,"iconBefore","","events",["^E",[["^3",["event","click","type","datasource","method","trigger","pluginId","runCommand","targetId",null,"params",["^3",[]],"waitType","debounce","waitMs","0"]]]],"loading",false,"loaderPosition","auto","disabled",false,"maintainSpaceWhenHidden",false]],"^;",["^3",[]],"^<",["^0",["^ ","n","position2","v",["^ ","^5","grid","^@","","^F","body","^G","","row",0,"col",0,"^H",1,"^I",2,"^J",0,"^K",null]]],"^=",null,"^>",null,"^?",null,"^@","","^A","~m1713225225403","^B","~m1713225233659","^C","","^D",null]]]]],"^A",null,"version","3.45.0","appThemeId",-1,"appThemeName",null,"appMaxWidth","1560px","preloadedAppJavaScript",null,"preloadedAppJSLinks",[],"testEntities",[],"tests",[],"appStyles","","responsiveLayoutDisabled",false,"loadingIndicatorsDisabled",false,"urlFragmentDefinitions",["^E",[]],"pageLoadValueOverrides",["^E",[]],"customDocumentTitle","","customDocumentTitleEnabled",false,"customShortcuts",[],"isGlobalWidget",false,"isMobileApp",false,"isFormApp",false,"shortlink",null,"multiScreenMobileApp",false,"mobileAppSettings",["^ ","mobileOfflineModeEnabled",false,"mobileOfflineModeDelaySync",false,"mobileOfflineModeBannerMode","default","displaySetting",["^ ","landscapeMode",false,"tabletMode",false]],"notificationsSettings",["^ ","globalQueryShowFailureToast",true,"globalQueryShowSuccessToast",false,"globalQueryToastDuration",4.5],"folders",["^E",[]],"queryStatusVisibility",false,"markdownLinkBehavior","auto","inAppRetoolPillAppearance","NO_OVERRIDE","rootScreen",null,"instrumentationEnabled",false,"experimentalFeatures",["^ ","sourceControlTemplateDehydration",false],"experimentalDataTabEnabled",true,"customComponentCollections",[],"savePlatform","web","internationalizationSettings",["^ ","internationalizationEnabled",false,"internationalizationFiles",[]]]]]"
},
"changesRecord": [
{
"type": "WIDGET_TEMPLATE_UPDATE",
"payload": {
"plugin": {
"id": "button1",
"type": "widget",
"uuid": "13a2446a-0162-4a64-a378-8b616c23e52d",
"style": {},
"folder": "",
"subtype": "ButtonWidget2",
"template": {
"text": "Start",
"events": [
{
"type": "datasource",
"event": "click",
"method": "trigger",
"params": {},
"waitMs": "0",
"pluginId": "runCommand",
"waitType": "debounce"
},
{
"type": "datasource",
"event": "click",
"method": "trigger",
"params": {},
"waitMs": "0",
"pluginId": "runCommand",
"waitType": "debounce"
}
],
"hidden": false,
"margin": "4px 8px",
"submit": false,
"loading": false,
"disabled": false,
"clickable": false,
"iconAfter": "",
"heightType": "fixed",
"iconBefore": "",
"tooltipText": "",
"showInEditor": false,
"styleVariant": "solid",
"loaderPosition": "auto",
"submitTargetId": null,
"horizontalAlign": "stretch",
"maintainSpaceWhenHidden": false
},
"container": "",
"createdAt": "2024-04-15T23:53:45.403Z",
"position2": {
"col": 0,
"row": 0,
"type": "grid",
"width": 2,
"height": 1,
"tabNum": 0,
"rowGroup": "body",
"container": "",
"subcontainer": "",
"stackPosition": null
},
"updatedAt": "2024-04-15T23:53:50.722Z"
},
"update": {
"events": [
{
"type": "datasource",
"event": "click",
"method": "trigger",
"params": {},
"waitMs": "0",
"pluginId": "runCommand",
"waitType": "debounce"
}
]
},
"widgetId": "button1",
"shouldRecalculateTemplate": true
},
"isUserTriggered": true
}
],
"gitSha": null,
"checksum": null,
"createdAt": "2024-04-15T23:53:55.817Z",
"updatedAt": "2024-04-15T23:53:55.817Z",
"pageId": 3054225,
"userId": 641953,
"branchId": null,
"page": {
"name": "unreliable setTimeout PoC"
}
},
"modules": {}
}

Copy-paste error:

should be

const sleep = t => new Promise(f => setTimeout(f, t))

Here is a timing of the response if I manually click in the app UI:

Start
Running for 1 seconds: 5108
Running for 2 seconds: 8685
Running for 3 seconds: 10468
Running for 4 seconds: 12300
Running for 5 seconds: 13332

If I just click the Start button and do not ever click the UI, I get:

Start
Running for 1 seconds: 3278
Running for 2 seconds: 14279
Running for 3 seconds: 18905
Running for 4 seconds: 23358
Running for 5 seconds: 32376

Hey @johnny2 if you're still looking into this, our Office Hours could be a good format for exploring what's happening here with the asynchronous JS, and get ideas in a group including Retool Community Support Engineers and other experts from the community :slightly_smiling_face:

If you do join an Office Hours session, could then share learnings back here as I'm sure it could be useful for others searching for how to build similar functionality!