Browse Source

Fix desktop effect race condition (#2313)

* don't poll desktop before the render has been applied

* fix desktop headless tests

* move edit queued code into the edit channel and add more comments
Evan Almloff 1 year ago
parent
commit
74352f2f61

+ 2 - 1
packages/desktop/headless_tests/events.rs

@@ -20,8 +20,9 @@ fn app() -> Element {
     let received = RECEIVED_EVENTS();
     let expected = utils::EXPECTED_EVENTS();
 
-    use_effect(move || {
+    use_memo(move || {
         println!("expecting {} events", utils::EXPECTED_EVENTS());
+        println!("received {} events", RECEIVED_EVENTS());
     });
 
     if expected != 0 && received == expected {

+ 39 - 1
packages/desktop/src/edits.rs

@@ -1,4 +1,5 @@
-use std::{cell::RefCell, collections::VecDeque, rc::Rc};
+use std::cell::Cell;
+use std::{cell::RefCell, collections::VecDeque, rc::Rc, task::Waker};
 
 /// This handles communication between the requests that the webview makes and the interpreter. The interpreter constantly makes long running requests to the webview to get any edits that should be made to the DOM almost like server side events.
 /// It will hold onto the requests until the interpreter is ready to handle them and hold onto any pending edits until a new request is made.
@@ -6,6 +7,11 @@ use std::{cell::RefCell, collections::VecDeque, rc::Rc};
 pub(crate) struct EditQueue {
     queue: Rc<RefCell<VecDeque<Vec<u8>>>>,
     responder: Rc<RefCell<Option<wry::RequestAsyncResponder>>>,
+    // Stores any futures waiting for edits to be applied to the webview
+    // NOTE: We don't use a Notify here because we need polling the notify to be cancel safe
+    waiting_for_edits_flushed: Rc<RefCell<Vec<Waker>>>,
+    // If this webview is currently waiting for an edit to be flushed. We don't run the virtual dom while this is true to avoid running effects before the dom has been updated
+    edits_in_progress: Rc<Cell<bool>>,
 }
 
 impl EditQueue {
@@ -14,16 +20,48 @@ impl EditQueue {
         if let Some(bytes) = queue.pop_back() {
             responder.respond(wry::http::Response::new(bytes));
         } else {
+            // There are now no edits that need to be applied to the webview
+            self.edits_finished();
             *self.responder.borrow_mut() = Some(responder);
         }
     }
 
     pub fn add_edits(&self, edits: Vec<u8>) {
         let mut responder = self.responder.borrow_mut();
+        // There are pending edits that need to be applied to the webview before we run futures
+        self.start_edits();
         if let Some(responder) = responder.take() {
             responder.respond(wry::http::Response::new(edits));
         } else {
             self.queue.borrow_mut().push_front(edits);
         }
     }
+
+    fn start_edits(&self) {
+        self.edits_in_progress.set(true);
+    }
+
+    fn edits_finished(&self) {
+        for waker in self.waiting_for_edits_flushed.borrow_mut().drain(..) {
+            waker.wake();
+        }
+        self.edits_in_progress.set(false);
+    }
+
+    fn edits_in_progress(&self) -> bool {
+        self.edits_in_progress.get()
+    }
+
+    /// Wait until all pending edits have been rendered in the webview
+    pub fn poll_edits_flushed(&self, cx: &mut std::task::Context<'_>) -> std::task::Poll<()> {
+        if self.edits_in_progress() {
+            let waker = cx.waker();
+            self.waiting_for_edits_flushed
+                .borrow_mut()
+                .push(waker.clone());
+            std::task::Poll::Pending
+        } else {
+            std::task::Poll::Ready(())
+        }
+    }
 }

+ 7 - 1
packages/desktop/src/webview.rs

@@ -229,10 +229,16 @@ impl WebviewInstance {
     pub fn poll_vdom(&mut self) {
         let mut cx = std::task::Context::from_waker(&self.waker);
 
-        // Continously poll the virtualdom until it's pending
+        // Continuously poll the virtualdom until it's pending
         // Wait for work will return Ready when it has edits to be sent to the webview
         // It will return Pending when it needs to be polled again - nothing is ready
         loop {
+            // If we're waiting for a render, wait for it to finish before we continue
+            let edits_flushed_poll = self.desktop_context.edit_queue.poll_edits_flushed(&mut cx);
+            if edits_flushed_poll.is_pending() {
+                return;
+            }
+
             {
                 let fut = self.dom.wait_for_work();
                 pin_mut!(fut);

+ 1 - 1
packages/interpreter/src/js/hash.txt

@@ -1 +1 @@
-2770005544568683192
+5713307201725207733

+ 1 - 1
packages/interpreter/src/js/native.js

@@ -1 +1 @@
-function retriveValues(event,target){let contents={values:{}},form=target.closest("form");if(form){if(event.type==="input"||event.type==="change"||event.type==="submit"||event.type==="reset"||event.type==="click")contents=retrieveFormValues(form)}return contents}function retrieveFormValues(form){const formData=new FormData(form),contents={};return formData.forEach((value,key)=>{if(contents[key])contents[key].push(value);else contents[key]=[value]}),{valid:form.checkValidity(),values:contents}}function retriveSelectValue(target){let options=target.selectedOptions,values=[];for(let i=0;i<options.length;i++)values.push(options[i].value);return values}function serializeEvent(event,target){let contents={},extend=(obj)=>contents={...contents,...obj};if(event instanceof WheelEvent)extend(serializeWheelEvent(event));if(event instanceof MouseEvent)extend(serializeMouseEvent(event));if(event instanceof KeyboardEvent)extend(serializeKeyboardEvent(event));if(event instanceof InputEvent)extend(serializeInputEvent(event,target));if(event instanceof PointerEvent)extend(serializePointerEvent(event));if(event instanceof AnimationEvent)extend(serializeAnimationEvent(event));if(event instanceof TransitionEvent)extend({property_name:event.propertyName,elapsed_time:event.elapsedTime,pseudo_element:event.pseudoElement});if(event instanceof CompositionEvent)extend({data:event.data});if(event instanceof DragEvent)extend(serializeDragEvent(event));if(event instanceof FocusEvent)extend({});if(event instanceof ClipboardEvent)extend({});if(typeof TouchEvent!=="undefined"&&event instanceof TouchEvent)extend(serializeTouchEvent(event));if(event.type==="submit"||event.type==="reset"||event.type==="click"||event.type==="change"||event.type==="input")extend(serializeInputEvent(event,target));if(event instanceof DragEvent);return contents}var serializeInputEvent=function(event,target){let contents={};if(target instanceof HTMLElement){let values=retriveValues(event,target);contents.values=values.values,contents.valid=values.valid}if(event.target instanceof HTMLInputElement){let target2=event.target,value=target2.value??target2.textContent??"";if(target2.type==="checkbox")value=target2.checked?"true":"false";else if(target2.type==="radio")value=target2.value;contents.value=value}if(event.target instanceof HTMLTextAreaElement)contents.value=event.target.value;if(event.target instanceof HTMLSelectElement)contents.value=retriveSelectValue(event.target).join(",");if(contents.value===void 0)contents.value="";return contents},serializeWheelEvent=function(event){return{delta_x:event.deltaX,delta_y:event.deltaY,delta_z:event.deltaZ,delta_mode:event.deltaMode}},serializeTouchEvent=function(event){return{alt_key:event.altKey,ctrl_key:event.ctrlKey,meta_key:event.metaKey,shift_key:event.shiftKey,changed_touches:event.changedTouches,target_touches:event.targetTouches,touches:event.touches}},serializePointerEvent=function(event){return{alt_key:event.altKey,button:event.button,buttons:event.buttons,client_x:event.clientX,client_y:event.clientY,ctrl_key:event.ctrlKey,meta_key:event.metaKey,page_x:event.pageX,page_y:event.pageY,screen_x:event.screenX,screen_y:event.screenY,shift_key:event.shiftKey,pointer_id:event.pointerId,width:event.width,height:event.height,pressure:event.pressure,tangential_pressure:event.tangentialPressure,tilt_x:event.tiltX,tilt_y:event.tiltY,twist:event.twist,pointer_type:event.pointerType,is_primary:event.isPrimary}},serializeMouseEvent=function(event){return{alt_key:event.altKey,button:event.button,buttons:event.buttons,client_x:event.clientX,client_y:event.clientY,ctrl_key:event.ctrlKey,meta_key:event.metaKey,offset_x:event.offsetX,offset_y:event.offsetY,page_x:event.pageX,page_y:event.pageY,screen_x:event.screenX,screen_y:event.screenY,shift_key:event.shiftKey}},serializeKeyboardEvent=function(event){return{char_code:event.charCode,is_composing:event.isComposing,key:event.key,alt_key:event.altKey,ctrl_key:event.ctrlKey,meta_key:event.metaKey,key_code:event.keyCode,shift_key:event.shiftKey,location:event.location,repeat:event.repeat,which:event.which,code:event.code}},serializeAnimationEvent=function(event){return{animation_name:event.animationName,elapsed_time:event.elapsedTime,pseudo_element:event.pseudoElement}},serializeDragEvent=function(event){let files=void 0;if(event.dataTransfer&&event.dataTransfer.files&&event.dataTransfer.files.length>0)files={files:{placeholder:[]}};return{mouse:{alt_key:event.altKey,ctrl_key:event.ctrlKey,meta_key:event.metaKey,shift_key:event.shiftKey,...serializeMouseEvent(event)},files}};var getTargetId=function(target){if(!(target instanceof Node))return null;let ourTarget=target,realId=null;while(realId==null){if(ourTarget===null)return null;if(ourTarget instanceof Element)realId=ourTarget.getAttribute("data-dioxus-id");ourTarget=ourTarget.parentNode}return parseInt(realId)},JSChannel_;if(RawInterpreter!==void 0&&RawInterpreter!==null)JSChannel_=RawInterpreter;class NativeInterpreter extends JSChannel_{intercept_link_redirects;ipc;editsPath;kickStylesheets;queuedBytes=[];liveview;constructor(editsPath){super();this.editsPath=editsPath,this.kickStylesheets=!1}initialize(root){this.intercept_link_redirects=!0,this.liveview=!1,window.addEventListener("dragover",function(e){if(e.target instanceof Element&&e.target.tagName!="INPUT")e.preventDefault()},!1),window.addEventListener("drop",function(e){if(!(e.target instanceof Element))return;e.preventDefault()},!1),window.addEventListener("click",(event)=>{const target=event.target;if(target instanceof HTMLInputElement&&target.getAttribute("type")==="file"){let target_id=getTargetId(target);if(target_id!==null){const message=this.serializeIpcMessage("file_dialog",{event:"change&input",accept:target.getAttribute("accept"),directory:target.getAttribute("webkitdirectory")==="true",multiple:target.hasAttribute("multiple"),target:target_id,bubbles:event.bubbles});this.ipc.postMessage(message),event.preventDefault()}}}),this.ipc=window.ipc;const handler=(event)=>this.handleEvent(event,event.type,!0);super.initialize(root,handler)}serializeIpcMessage(method,params={}){return JSON.stringify({method,params})}scrollTo(id,behavior){const node=this.nodes[id];if(node instanceof HTMLElement)node.scrollIntoView({behavior})}getClientRect(id){const node=this.nodes[id];if(node instanceof HTMLElement){const rect=node.getBoundingClientRect();return{type:"GetClientRect",origin:[rect.x,rect.y],size:[rect.width,rect.height]}}}setFocus(id,focus){const node=this.nodes[id];if(node instanceof HTMLElement)if(focus)node.focus();else node.blur()}loadChild(array){let node=this.stack[this.stack.length-1];for(let i=0;i<array.length;i++){let end=array[i];for(node=node.firstChild;end>0;end--)node=node.nextSibling}return node}appendChildren(id,many){const root=this.nodes[id],els=this.stack.splice(this.stack.length-many);for(let k=0;k<many;k++)root.appendChild(els[k])}handleEvent(event,name,bubbles){const target=event.target,realId=getTargetId(target),contents=serializeEvent(event,target);let body={name,data:contents,element:realId,bubbles};if(this.preventDefaults(event,target),this.liveview){if(target instanceof HTMLInputElement&&(event.type==="change"||event.type==="input")){if(target.getAttribute("type")==="file")this.readFiles(target,contents,bubbles,realId,name)}}else{const message=this.serializeIpcMessage("user_event",body);this.ipc.postMessage(message)}}preventDefaults(event,target){let preventDefaultRequests=null;if(target instanceof Element)preventDefaultRequests=target.getAttribute("dioxus-prevent-default");if(preventDefaultRequests&&preventDefaultRequests.includes(`on${event.type}`))event.preventDefault();if(event.type==="submit")event.preventDefault();if(target instanceof Element&&event.type==="click")this.handleClickNavigate(event,target,preventDefaultRequests)}handleClickNavigate(event,target,preventDefaultRequests){if(!this.intercept_link_redirects)return;if(target.tagName==="BUTTON"&&event.type=="submit")event.preventDefault();let a_element=target.closest("a");if(a_element==null)return;event.preventDefault();let elementShouldPreventDefault=preventDefaultRequests&&preventDefaultRequests.includes("onclick"),aElementShouldPreventDefault=a_element.getAttribute("dioxus-prevent-default"),linkShouldPreventDefault=aElementShouldPreventDefault&&aElementShouldPreventDefault.includes("onclick");if(!elementShouldPreventDefault&&!linkShouldPreventDefault){const href=a_element.getAttribute("href");if(href!==""&&href!==null&&href!==void 0)this.ipc.postMessage(this.serializeIpcMessage("browser_open",{href}))}}enqueueBytes(bytes){this.queuedBytes.push(bytes)}flushQueuedBytes(){const byteArray=this.queuedBytes;this.queuedBytes=[];for(let bytes of byteArray)this.run_from_bytes(bytes)}waitForRequest(headless){fetch(new Request(this.editsPath)).then((response)=>response.arrayBuffer()).then((bytes)=>{if(headless)this.run_from_bytes(bytes);else this.enqueueBytes(bytes),requestAnimationFrame(()=>{this.flushQueuedBytes()});this.waitForRequest(headless)})}kickAllStylesheetsOnPage(){let stylesheets=document.querySelectorAll("link[rel=stylesheet]");for(let i=0;i<stylesheets.length;i++){let sheet=stylesheets[i];fetch(sheet.href,{cache:"reload"}).then(()=>{sheet.href=sheet.href+"?"+Math.random()})}}async readFiles(target,contents,bubbles,realId,name){let files=target.files,file_contents={};for(let i=0;i<files.length;i++){const file=files[i];file_contents[file.name]=Array.from(new Uint8Array(await file.arrayBuffer()))}contents.files={files:file_contents};const message=this.serializeIpcMessage("user_event",{name,element:realId,data:contents,bubbles});this.ipc.postMessage(message)}}export{NativeInterpreter};
+function retriveValues(event,target){let contents={values:{}},form=target.closest("form");if(form){if(event.type==="input"||event.type==="change"||event.type==="submit"||event.type==="reset"||event.type==="click")contents=retrieveFormValues(form)}return contents}function retrieveFormValues(form){const formData=new FormData(form),contents={};return formData.forEach((value,key)=>{if(contents[key])contents[key].push(value);else contents[key]=[value]}),{valid:form.checkValidity(),values:contents}}function retriveSelectValue(target){let options=target.selectedOptions,values=[];for(let i=0;i<options.length;i++)values.push(options[i].value);return values}function serializeEvent(event,target){let contents={},extend=(obj)=>contents={...contents,...obj};if(event instanceof WheelEvent)extend(serializeWheelEvent(event));if(event instanceof MouseEvent)extend(serializeMouseEvent(event));if(event instanceof KeyboardEvent)extend(serializeKeyboardEvent(event));if(event instanceof InputEvent)extend(serializeInputEvent(event,target));if(event instanceof PointerEvent)extend(serializePointerEvent(event));if(event instanceof AnimationEvent)extend(serializeAnimationEvent(event));if(event instanceof TransitionEvent)extend({property_name:event.propertyName,elapsed_time:event.elapsedTime,pseudo_element:event.pseudoElement});if(event instanceof CompositionEvent)extend({data:event.data});if(event instanceof DragEvent)extend(serializeDragEvent(event));if(event instanceof FocusEvent)extend({});if(event instanceof ClipboardEvent)extend({});if(typeof TouchEvent!=="undefined"&&event instanceof TouchEvent)extend(serializeTouchEvent(event));if(event.type==="submit"||event.type==="reset"||event.type==="click"||event.type==="change"||event.type==="input")extend(serializeInputEvent(event,target));if(event instanceof DragEvent);return contents}var serializeInputEvent=function(event,target){let contents={};if(target instanceof HTMLElement){let values=retriveValues(event,target);contents.values=values.values,contents.valid=values.valid}if(event.target instanceof HTMLInputElement){let target2=event.target,value=target2.value??target2.textContent??"";if(target2.type==="checkbox")value=target2.checked?"true":"false";else if(target2.type==="radio")value=target2.value;contents.value=value}if(event.target instanceof HTMLTextAreaElement)contents.value=event.target.value;if(event.target instanceof HTMLSelectElement)contents.value=retriveSelectValue(event.target).join(",");if(contents.value===void 0)contents.value="";return contents},serializeWheelEvent=function(event){return{delta_x:event.deltaX,delta_y:event.deltaY,delta_z:event.deltaZ,delta_mode:event.deltaMode}},serializeTouchEvent=function(event){return{alt_key:event.altKey,ctrl_key:event.ctrlKey,meta_key:event.metaKey,shift_key:event.shiftKey,changed_touches:event.changedTouches,target_touches:event.targetTouches,touches:event.touches}},serializePointerEvent=function(event){return{alt_key:event.altKey,button:event.button,buttons:event.buttons,client_x:event.clientX,client_y:event.clientY,ctrl_key:event.ctrlKey,meta_key:event.metaKey,page_x:event.pageX,page_y:event.pageY,screen_x:event.screenX,screen_y:event.screenY,shift_key:event.shiftKey,pointer_id:event.pointerId,width:event.width,height:event.height,pressure:event.pressure,tangential_pressure:event.tangentialPressure,tilt_x:event.tiltX,tilt_y:event.tiltY,twist:event.twist,pointer_type:event.pointerType,is_primary:event.isPrimary}},serializeMouseEvent=function(event){return{alt_key:event.altKey,button:event.button,buttons:event.buttons,client_x:event.clientX,client_y:event.clientY,ctrl_key:event.ctrlKey,meta_key:event.metaKey,offset_x:event.offsetX,offset_y:event.offsetY,page_x:event.pageX,page_y:event.pageY,screen_x:event.screenX,screen_y:event.screenY,shift_key:event.shiftKey}},serializeKeyboardEvent=function(event){return{char_code:event.charCode,is_composing:event.isComposing,key:event.key,alt_key:event.altKey,ctrl_key:event.ctrlKey,meta_key:event.metaKey,key_code:event.keyCode,shift_key:event.shiftKey,location:event.location,repeat:event.repeat,which:event.which,code:event.code}},serializeAnimationEvent=function(event){return{animation_name:event.animationName,elapsed_time:event.elapsedTime,pseudo_element:event.pseudoElement}},serializeDragEvent=function(event){let files=void 0;if(event.dataTransfer&&event.dataTransfer.files&&event.dataTransfer.files.length>0)files={files:{placeholder:[]}};return{mouse:{alt_key:event.altKey,ctrl_key:event.ctrlKey,meta_key:event.metaKey,shift_key:event.shiftKey,...serializeMouseEvent(event)},files}};var getTargetId=function(target){if(!(target instanceof Node))return null;let ourTarget=target,realId=null;while(realId==null){if(ourTarget===null)return null;if(ourTarget instanceof Element)realId=ourTarget.getAttribute("data-dioxus-id");ourTarget=ourTarget.parentNode}return parseInt(realId)},JSChannel_;if(RawInterpreter!==void 0&&RawInterpreter!==null)JSChannel_=RawInterpreter;class NativeInterpreter extends JSChannel_{intercept_link_redirects;ipc;editsPath;kickStylesheets;queuedBytes=[];liveview;constructor(editsPath){super();this.editsPath=editsPath,this.kickStylesheets=!1}initialize(root){this.intercept_link_redirects=!0,this.liveview=!1,window.addEventListener("dragover",function(e){if(e.target instanceof Element&&e.target.tagName!="INPUT")e.preventDefault()},!1),window.addEventListener("drop",function(e){if(!(e.target instanceof Element))return;e.preventDefault()},!1),window.addEventListener("click",(event)=>{const target=event.target;if(target instanceof HTMLInputElement&&target.getAttribute("type")==="file"){let target_id=getTargetId(target);if(target_id!==null){const message=this.serializeIpcMessage("file_dialog",{event:"change&input",accept:target.getAttribute("accept"),directory:target.getAttribute("webkitdirectory")==="true",multiple:target.hasAttribute("multiple"),target:target_id,bubbles:event.bubbles});this.ipc.postMessage(message),event.preventDefault()}}}),this.ipc=window.ipc;const handler=(event)=>this.handleEvent(event,event.type,!0);super.initialize(root,handler)}serializeIpcMessage(method,params={}){return JSON.stringify({method,params})}scrollTo(id,behavior){const node=this.nodes[id];if(node instanceof HTMLElement)node.scrollIntoView({behavior})}getClientRect(id){const node=this.nodes[id];if(node instanceof HTMLElement){const rect=node.getBoundingClientRect();return{type:"GetClientRect",origin:[rect.x,rect.y],size:[rect.width,rect.height]}}}setFocus(id,focus){const node=this.nodes[id];if(node instanceof HTMLElement)if(focus)node.focus();else node.blur()}loadChild(array){let node=this.stack[this.stack.length-1];for(let i=0;i<array.length;i++){let end=array[i];for(node=node.firstChild;end>0;end--)node=node.nextSibling}return node}appendChildren(id,many){const root=this.nodes[id],els=this.stack.splice(this.stack.length-many);for(let k=0;k<many;k++)root.appendChild(els[k])}handleEvent(event,name,bubbles){const target=event.target,realId=getTargetId(target),contents=serializeEvent(event,target);let body={name,data:contents,element:realId,bubbles};if(this.preventDefaults(event,target),this.liveview){if(target instanceof HTMLInputElement&&(event.type==="change"||event.type==="input")){if(target.getAttribute("type")==="file")this.readFiles(target,contents,bubbles,realId,name)}}else{const message=this.serializeIpcMessage("user_event",body);this.ipc.postMessage(message)}}preventDefaults(event,target){let preventDefaultRequests=null;if(target instanceof Element)preventDefaultRequests=target.getAttribute("dioxus-prevent-default");if(preventDefaultRequests&&preventDefaultRequests.includes(`on${event.type}`))event.preventDefault();if(event.type==="submit")event.preventDefault();if(target instanceof Element&&event.type==="click")this.handleClickNavigate(event,target,preventDefaultRequests)}handleClickNavigate(event,target,preventDefaultRequests){if(!this.intercept_link_redirects)return;if(target.tagName==="BUTTON"&&event.type=="submit")event.preventDefault();let a_element=target.closest("a");if(a_element==null)return;event.preventDefault();let elementShouldPreventDefault=preventDefaultRequests&&preventDefaultRequests.includes("onclick"),aElementShouldPreventDefault=a_element.getAttribute("dioxus-prevent-default"),linkShouldPreventDefault=aElementShouldPreventDefault&&aElementShouldPreventDefault.includes("onclick");if(!elementShouldPreventDefault&&!linkShouldPreventDefault){const href=a_element.getAttribute("href");if(href!==""&&href!==null&&href!==void 0)this.ipc.postMessage(this.serializeIpcMessage("browser_open",{href}))}}enqueueBytes(bytes){this.queuedBytes.push(bytes)}flushQueuedBytes(){const byteArray=this.queuedBytes;this.queuedBytes=[];for(let bytes of byteArray)this.run_from_bytes(bytes)}rafEdits(headless,bytes){if(headless)this.run_from_bytes(bytes),this.waitForRequest(headless);else this.enqueueBytes(bytes),requestAnimationFrame(()=>{this.flushQueuedBytes(),this.waitForRequest(headless)})}waitForRequest(headless){fetch(new Request(this.editsPath)).then((response)=>response.arrayBuffer()).then((bytes)=>{this.rafEdits(headless,bytes)})}kickAllStylesheetsOnPage(){let stylesheets=document.querySelectorAll("link[rel=stylesheet]");for(let i=0;i<stylesheets.length;i++){let sheet=stylesheets[i];fetch(sheet.href,{cache:"reload"}).then(()=>{sheet.href=sheet.href+"?"+Math.random()})}}async readFiles(target,contents,bubbles,realId,name){let files=target.files,file_contents={};for(let i=0;i<files.length;i++){const file=files[i];file_contents[file.name]=Array.from(new Uint8Array(await file.arrayBuffer()))}contents.files={files:file_contents};const message=this.serializeIpcMessage("user_event",{name,element:realId,data:contents,bubbles});this.ipc.postMessage(message)}}export{NativeInterpreter};

+ 72 - 41
packages/interpreter/src/ts/native.ts

@@ -15,7 +15,7 @@ var JSChannel_: typeof BaseInterpreter;
 if (RawInterpreter !== undefined && RawInterpreter !== null) {
   // @ts-ignore - this is coming from the host
   JSChannel_ = RawInterpreter;
-};
+}
 
 export class NativeInterpreter extends JSChannel_ {
   intercept_link_redirects: boolean;
@@ -40,28 +40,39 @@ export class NativeInterpreter extends JSChannel_ {
 
     // attach an event listener on the body that prevents file drops from navigating
     // this is because the browser will try to navigate to the file if it's dropped on the window
-    window.addEventListener("dragover", function (e) {
-      // // check which element is our target
-      if (e.target instanceof Element && e.target.tagName != "INPUT") {
-        e.preventDefault();
-      }
-    }, false);
+    window.addEventListener(
+      "dragover",
+      function (e) {
+        // // check which element is our target
+        if (e.target instanceof Element && e.target.tagName != "INPUT") {
+          e.preventDefault();
+        }
+      },
+      false
+    );
 
-    window.addEventListener("drop", function (e) {
-      let target = e.target;
+    window.addEventListener(
+      "drop",
+      function (e) {
+        let target = e.target;
 
-      if (!(target instanceof Element)) {
-        return;
-      }
+        if (!(target instanceof Element)) {
+          return;
+        }
 
-      // Dropping a file on the window will navigate to the file, which we don't want
-      e.preventDefault();
-    }, false);
+        // Dropping a file on the window will navigate to the file, which we don't want
+        e.preventDefault();
+      },
+      false
+    );
 
     // attach a listener to the route that listens for clicks and prevents the default file dialog
     window.addEventListener("click", (event) => {
       const target = event.target;
-      if (target instanceof HTMLInputElement && target.getAttribute("type") === "file") {
+      if (
+        target instanceof HTMLInputElement &&
+        target.getAttribute("type") === "file"
+      ) {
         // Send a message to the host to open the file dialog if the target is a file input and has a dioxus id attached to it
         let target_id = getTargetId(target);
         if (target_id !== null) {
@@ -79,12 +90,12 @@ export class NativeInterpreter extends JSChannel_ {
       }
     });
 
-
     // @ts-ignore - wry gives us this
     this.ipc = window.ipc;
 
     // make sure we pass the handler to the base interpreter
-    const handler: EventListener = (event) => this.handleEvent(event, event.type, true);
+    const handler: EventListener = (event) =>
+      this.handleEvent(event, event.type, true);
     super.initialize(root, handler);
   }
 
@@ -99,7 +110,9 @@ export class NativeInterpreter extends JSChannel_ {
     }
   }
 
-  getClientRect(id: NodeId): { type: string; origin: number[]; size: number[]; } | undefined {
+  getClientRect(
+    id: NodeId
+  ): { type: string; origin: number[]; size: number[] } | undefined {
     const node = this.nodes[id];
     if (node instanceof HTMLElement) {
       const rect = node.getBoundingClientRect();
@@ -171,13 +184,15 @@ export class NativeInterpreter extends JSChannel_ {
     // liveview does not have syncronous event handling, so we need to send the event to the host
     if (this.liveview) {
       // Okay, so the user might've requested some files to be read
-      if (target instanceof HTMLInputElement && (event.type === "change" || event.type === "input")) {
+      if (
+        target instanceof HTMLInputElement &&
+        (event.type === "change" || event.type === "input")
+      ) {
         if (target.getAttribute("type") === "file") {
           this.readFiles(target, contents, bubbles, realId, name);
         }
       }
     } else {
-
       const message = this.serializeIpcMessage("user_event", body);
       this.ipc.postMessage(message);
 
@@ -195,8 +210,6 @@ export class NativeInterpreter extends JSChannel_ {
     }
   }
 
-
-
   // This should:
   // - prevent form submissions from navigating
   // - prevent anchor tags from navigating
@@ -210,7 +223,10 @@ export class NativeInterpreter extends JSChannel_ {
       preventDefaultRequests = target.getAttribute(`dioxus-prevent-default`);
     }
 
-    if (preventDefaultRequests && preventDefaultRequests.includes(`on${event.type}`)) {
+    if (
+      preventDefaultRequests &&
+      preventDefaultRequests.includes(`on${event.type}`)
+    ) {
       event.preventDefault();
     }
 
@@ -224,7 +240,11 @@ export class NativeInterpreter extends JSChannel_ {
     }
   }
 
-  handleClickNavigate(event: Event, target: Element, preventDefaultRequests: string) {
+  handleClickNavigate(
+    event: Event,
+    target: Element,
+    preventDefaultRequests: string
+  ) {
     // todo call prevent default if it's the right type of event
     if (!this.intercept_link_redirects) {
       return;
@@ -279,24 +299,30 @@ export class NativeInterpreter extends JSChannel_ {
     }
   }
 
-  waitForRequest(headless: boolean) {
-    fetch(new Request(this.editsPath))
-      .then(response => response.arrayBuffer())
-      .then(bytes => {
-        // In headless mode, the requestAnimationFrame callback is never called, so we need to run the bytes directly
-        if (headless) {
-          // @ts-ignore
-          this.run_from_bytes(bytes);
-        } else {
-          this.enqueueBytes(bytes);
-          requestAnimationFrame(() => {
-            this.flushQueuedBytes();
-          });
-        }
+  // Run the edits the next animation frame
+  rafEdits(headless: boolean, bytes: ArrayBuffer) {
+    // In headless mode, the requestAnimationFrame callback is never called, so we need to run the bytes directly
+    if (headless) {
+      // @ts-ignore
+      this.run_from_bytes(bytes);
+      this.waitForRequest(headless);
+    } else {
+      this.enqueueBytes(bytes);
+      requestAnimationFrame(() => {
+        this.flushQueuedBytes();
+        // With request animation frames, we use the next reqwest as a marker to know when the frame is done and it is safe to run effects
         this.waitForRequest(headless);
       });
+    }
   }
 
+  waitForRequest(headless: boolean) {
+    fetch(new Request(this.editsPath))
+      .then((response) => response.arrayBuffer())
+      .then((bytes) => {
+        this.rafEdits(headless, bytes);
+      });
+  }
 
   kickAllStylesheetsOnPage() {
     // If this function is being called and we have not explicitly set kickStylesheets to true, then we should
@@ -314,7 +340,13 @@ export class NativeInterpreter extends JSChannel_ {
 
   //  A liveview only function
   // Desktop will intercept the event before it hits this
-  async readFiles(target: HTMLInputElement, contents: SerializedEvent, bubbles: boolean, realId: NodeId, name: string) {
+  async readFiles(
+    target: HTMLInputElement,
+    contents: SerializedEvent,
+    bubbles: boolean,
+    realId: NodeId,
+    name: string
+  ) {
     let files = target.files!;
     let file_contents: { [name: string]: number[] } = {};
 
@@ -388,7 +420,6 @@ function getTargetId(target: EventTarget): NodeId | null {
   return parseInt(realId);
 }
 
-
 // function applyFileUpload() {
 //   let inputs = document.querySelectorAll("input");
 //   for (let input of inputs) {