Просмотр исходного кода

switch liveview to sledgehammer

Evan Almloff 1 год назад
Родитель
Сommit
f20b740abe

+ 19 - 0
packages/desktop/src/desktop_context.rs

@@ -14,6 +14,7 @@ use dioxus_core::ScopeState;
 use dioxus_core::VirtualDom;
 #[cfg(all(feature = "hot-reload", debug_assertions))]
 use dioxus_hot_reload::HotReloadMsg;
+use dioxus_interpreter_js::Channel;
 use slab::Slab;
 use wry::application::accelerator::Accelerator;
 use wry::application::event::Event;
@@ -35,6 +36,18 @@ pub fn use_window(cx: &ScopeState) -> &DesktopContext {
         .unwrap()
 }
 
+struct EditQueue {
+    queue: Vec<Vec<u8>>,
+}
+
+impl EditQueue {
+    fn push(&mut self, channel: &mut Channel) {
+        let iter = channel.export_memory();
+        self.queue.push(iter.collect());
+        channel.reset();
+    }
+}
+
 pub(crate) type WebviewQueue = Rc<RefCell<Vec<WebviewHandler>>>;
 
 /// An imperative interface to the current window.
@@ -67,6 +80,10 @@ pub struct DesktopService {
 
     pub(crate) shortcut_manager: ShortcutRegistry,
 
+    pub(crate) event_queue: Rc<RefCell<Vec<Vec<u8>>>>,
+
+    pub(crate) channel: Channel,
+
     #[cfg(target_os = "ios")]
     pub(crate) views: Rc<RefCell<Vec<*mut objc::runtime::Object>>>,
 }
@@ -100,6 +117,8 @@ impl DesktopService {
             pending_windows: webviews,
             event_handlers,
             shortcut_manager,
+            event_queue: Rc::new(RefCell::new(Vec::new())),
+            channel: Channel::new(),
             #[cfg(target_os = "ios")]
             views: Default::default(),
         }

+ 3 - 3
packages/interpreter/Cargo.toml

@@ -14,13 +14,13 @@ keywords = ["dom", "ui", "gui", "react", "wasm"]
 wasm-bindgen = { workspace = true, optional = true }
 js-sys = { version = "0.3.56", optional = true }
 web-sys = { version = "0.3.56", optional = true, features = ["Element", "Node"] }
-sledgehammer_bindgen = { version = "0.2.1", optional = true }
+sledgehammer_bindgen = { path = "/Users/evanalmloff/Desktop/Github/sledgehammer_bindgen", default-features = false, optional = true }
 sledgehammer_utils = { version = "0.2", optional = true }
 serde = { version = "1.0", features = ["derive"], optional = true }
 
 [features]
 default = []
 serialize = ["serde"]
-web = ["wasm-bindgen", "js-sys", "web-sys"]
-sledgehammer = ["wasm-bindgen", "js-sys", "web-sys", "sledgehammer_bindgen", "sledgehammer_utils"]
+sledgehammer = ["sledgehammer_bindgen", "sledgehammer_utils"]
+web = ["sledgehammer", "wasm-bindgen", "js-sys", "web-sys", "sledgehammer_bindgen/web"]
 minimal_bindings = []

+ 0 - 83
packages/interpreter/src/bindings.rs

@@ -1,83 +0,0 @@
-#![allow(clippy::unused_unit, non_upper_case_globals)]
-
-use js_sys::Function;
-use wasm_bindgen::prelude::*;
-use web_sys::Element;
-
-#[wasm_bindgen(module = "/src/interpreter.js")]
-extern "C" {
-    pub type InterpreterConfig;
-    #[wasm_bindgen(constructor)]
-    pub fn new(intercept_link_redirects: bool) -> InterpreterConfig;
-
-    pub type Interpreter;
-
-    #[wasm_bindgen(constructor)]
-    pub fn new(arg: Element, config: InterpreterConfig) -> Interpreter;
-
-    #[wasm_bindgen(method)]
-    pub fn SaveTemplate(this: &Interpreter, template: JsValue);
-
-    #[wasm_bindgen(method)]
-    pub fn MountToRoot(this: &Interpreter);
-
-    #[wasm_bindgen(method)]
-    pub fn AssignId(this: &Interpreter, path: &[u8], id: u32);
-
-    #[wasm_bindgen(method)]
-    pub fn CreatePlaceholder(this: &Interpreter, id: u32);
-
-    #[wasm_bindgen(method)]
-    pub fn CreateTextNode(this: &Interpreter, value: JsValue, id: u32);
-
-    #[wasm_bindgen(method)]
-    pub fn HydrateText(this: &Interpreter, path: &[u8], value: &str, id: u32);
-
-    #[wasm_bindgen(method)]
-    pub fn LoadTemplate(this: &Interpreter, name: &str, index: u32, id: u32);
-
-    #[wasm_bindgen(method)]
-    pub fn ReplaceWith(this: &Interpreter, id: u32, m: u32);
-
-    #[wasm_bindgen(method)]
-    pub fn ReplacePlaceholder(this: &Interpreter, path: &[u8], m: u32);
-
-    #[wasm_bindgen(method)]
-    pub fn InsertAfter(this: &Interpreter, id: u32, n: u32);
-
-    #[wasm_bindgen(method)]
-    pub fn InsertBefore(this: &Interpreter, id: u32, n: u32);
-
-    #[wasm_bindgen(method)]
-    pub fn SetAttribute(this: &Interpreter, id: u32, name: &str, value: JsValue, ns: Option<&str>);
-
-    #[wasm_bindgen(method)]
-    pub fn SetBoolAttribute(this: &Interpreter, id: u32, name: &str, value: bool);
-
-    #[wasm_bindgen(method)]
-    pub fn SetText(this: &Interpreter, id: u32, text: JsValue);
-
-    #[wasm_bindgen(method)]
-    pub fn NewEventListener(
-        this: &Interpreter,
-        name: &str,
-        id: u32,
-        bubbles: bool,
-        handler: &Function,
-    );
-
-    #[wasm_bindgen(method)]
-    pub fn RemoveEventListener(this: &Interpreter, name: &str, id: u32);
-
-    #[wasm_bindgen(method)]
-    pub fn RemoveAttribute(this: &Interpreter, id: u32, field: &str, ns: Option<&str>);
-
-    #[wasm_bindgen(method)]
-    pub fn Remove(this: &Interpreter, id: u32);
-
-    #[wasm_bindgen(method)]
-    pub fn PushRoot(this: &Interpreter, id: u32);
-
-    #[wasm_bindgen(method)]
-    pub fn AppendChildren(this: &Interpreter, id: u32, m: u32);
-}

+ 271 - 380
packages/interpreter/src/interpreter.js

@@ -1,390 +1,11 @@
 import { setAttributeInner } from "./common.js";
 
-class ListenerMap {
-  constructor(root) {
-    // bubbling events can listen at the root element
-    this.global = {};
-    // non bubbling events listen at the element the listener was created at
-    this.local = {};
-    this.root = root;
-  }
-
-  create(event_name, element, handler, bubbles) {
-    if (bubbles) {
-      if (this.global[event_name] === undefined) {
-        this.global[event_name] = {};
-        this.global[event_name].active = 1;
-        this.global[event_name].callback = handler;
-        this.root.addEventListener(event_name, handler);
-      } else {
-        this.global[event_name].active++;
-      }
-    } else {
-      const id = element.getAttribute("data-dioxus-id");
-      if (!this.local[id]) {
-        this.local[id] = {};
-      }
-      this.local[id][event_name] = handler;
-      element.addEventListener(event_name, handler);
-    }
-  }
-
-  remove(element, event_name, bubbles) {
-    if (bubbles) {
-      this.global[event_name].active--;
-      if (this.global[event_name].active === 0) {
-        this.root.removeEventListener(
-          event_name,
-          this.global[event_name].callback
-        );
-        delete this.global[event_name];
-      }
-    } else {
-      const id = element.getAttribute("data-dioxus-id");
-      delete this.local[id][event_name];
-      if (this.local[id].length === 0) {
-        delete this.local[id];
-      }
-      element.removeEventListener(event_name, handler);
-    }
-  }
-
-  removeAllNonBubbling(element) {
-    const id = element.getAttribute("data-dioxus-id");
-    delete this.local[id];
-  }
-}
-
 class InterpreterConfig {
   constructor(intercept_link_redirects) {
     this.intercept_link_redirects = intercept_link_redirects;
   }
 }
 
-class Interpreter {
-  constructor(root, config) {
-    this.config = config;
-    this.root = root;
-    this.listeners = new ListenerMap(root);
-    this.nodes = [root];
-    this.stack = [root];
-    this.handlers = {};
-    this.templates = {};
-    this.lastNodeWasText = false;
-  }
-  top() {
-    return this.stack[this.stack.length - 1];
-  }
-  pop() {
-    return this.stack.pop();
-  }
-  MountToRoot() {
-    this.AppendChildren(this.stack.length - 1);
-  }
-  SetNode(id, node) {
-    this.nodes[id] = node;
-  }
-  PushRoot(root) {
-    const node = this.nodes[root];
-    this.stack.push(node);
-  }
-  PopRoot() {
-    this.stack.pop();
-  }
-  AppendChildren(many) {
-    // let root = this.nodes[id];
-    let root = this.stack[this.stack.length - 1 - many];
-    let to_add = this.stack.splice(this.stack.length - many);
-    for (let i = 0; i < many; i++) {
-      root.appendChild(to_add[i]);
-    }
-  }
-  ReplaceWith(root_id, m) {
-    let root = this.nodes[root_id];
-    let els = this.stack.splice(this.stack.length - m);
-    if (is_element_node(root.nodeType)) {
-      this.listeners.removeAllNonBubbling(root);
-    }
-    root.replaceWith(...els);
-  }
-  InsertAfter(root, n) {
-    let old = this.nodes[root];
-    let new_nodes = this.stack.splice(this.stack.length - n);
-    old.after(...new_nodes);
-  }
-  InsertBefore(root, n) {
-    let old = this.nodes[root];
-    let new_nodes = this.stack.splice(this.stack.length - n);
-    old.before(...new_nodes);
-  }
-  Remove(root) {
-    let node = this.nodes[root];
-    if (node !== undefined) {
-      if (is_element_node(node)) {
-        this.listeners.removeAllNonBubbling(node);
-      }
-      node.remove();
-    }
-  }
-  CreateTextNode(text, root) {
-    const node = document.createTextNode(text);
-    this.nodes[root] = node;
-    this.stack.push(node);
-  }
-  CreatePlaceholder(root) {
-    let el = document.createElement("pre");
-    el.hidden = true;
-    this.stack.push(el);
-    this.nodes[root] = el;
-  }
-  NewEventListener(event_name, root, bubbles, handler) {
-    const element = this.nodes[root];
-    element.setAttribute("data-dioxus-id", `${root}`);
-    this.listeners.create(event_name, element, handler, bubbles);
-  }
-  RemoveEventListener(root, event_name, bubbles) {
-    const element = this.nodes[root];
-    element.removeAttribute(`data-dioxus-id`);
-    this.listeners.remove(element, event_name, bubbles);
-  }
-  SetText(root, text) {
-    this.nodes[root].textContent = text;
-  }
-  SetAttribute(id, field, value, ns) {
-    if (value === null) {
-      this.RemoveAttribute(id, field, ns);
-    } else {
-      const node = this.nodes[id];
-      setAttributeInner(node, field, value, ns);
-    }
-  }
-  RemoveAttribute(root, field, ns) {
-    const node = this.nodes[root];
-    if (!ns) {
-      switch (field) {
-        case "value":
-          node.value = "";
-          break;
-        case "checked":
-          node.checked = false;
-          break;
-        case "selected":
-          node.selected = false;
-          break;
-        case "dangerous_inner_html":
-          node.innerHTML = "";
-          break;
-        default:
-          node.removeAttribute(field);
-          break;
-      }
-    } else if (ns == "style") {
-      node.style.removeProperty(name);
-    } else {
-      node.removeAttributeNS(ns, field);
-    }
-  }
-
-  GetClientRect(id) {
-    const node = this.nodes[id];
-    if (!node) {
-      return;
-    }
-    const rect = node.getBoundingClientRect();
-    return {
-      type: "GetClientRect",
-      origin: [rect.x, rect.y],
-      size: [rect.width, rect.height],
-    };
-  }
-
-  ScrollTo(id, behavior) {
-    const node = this.nodes[id];
-    if (!node) {
-      return false;
-    }
-    node.scrollIntoView({
-      behavior: behavior,
-    });
-    return true;
-  }
-
-  /// Set the focus on the element
-  SetFocus(id, focus) {
-    const node = this.nodes[id];
-    if (!node) {
-      return false;
-    }
-    if (focus) {
-      node.focus();
-    } else {
-      node.blur();
-    }
-    return true;
-  }
-
-  handleEdits(edits) {
-    for (let template of edits.templates) {
-      this.SaveTemplate(template);
-    }
-
-    for (let edit of edits.edits) {
-      this.handleEdit(edit);
-    }
-
-    /*POST_HANDLE_EDITS*/
-  }
-
-  SaveTemplate(template) {
-    let roots = [];
-    for (let root of template.roots) {
-      roots.push(this.MakeTemplateNode(root));
-    }
-    this.templates[template.name] = roots;
-  }
-
-  MakeTemplateNode(node) {
-    switch (node.type) {
-      case "Text":
-        return document.createTextNode(node.text);
-      case "Dynamic":
-        let dyn = document.createElement("pre");
-        dyn.hidden = true;
-        return dyn;
-      case "DynamicText":
-        return document.createTextNode("placeholder");
-      case "Element":
-        let el;
-
-        if (node.namespace != null) {
-          el = document.createElementNS(node.namespace, node.tag);
-        } else {
-          el = document.createElement(node.tag);
-        }
-
-        for (let attr of node.attrs) {
-          if (attr.type == "Static") {
-            setAttributeInner(el, attr.name, attr.value, attr.namespace);
-          }
-        }
-
-        for (let child of node.children) {
-          el.appendChild(this.MakeTemplateNode(child));
-        }
-
-        return el;
-    }
-  }
-  AssignId(path, id) {
-    this.nodes[id] = this.LoadChild(path);
-  }
-  LoadChild(path) {
-    // iterate through each number and get that child
-    let node = this.stack[this.stack.length - 1];
-
-    for (let i = 0; i < path.length; i++) {
-      node = node.childNodes[path[i]];
-    }
-
-    return node;
-  }
-  HydrateText(path, value, id) {
-    let node = this.LoadChild(path);
-
-    if (node.nodeType == Node.TEXT_NODE) {
-      node.textContent = value;
-    } else {
-      // replace with a textnode
-      let text = document.createTextNode(value);
-      node.replaceWith(text);
-      node = text;
-    }
-
-    this.nodes[id] = node;
-  }
-  ReplacePlaceholder(path, m) {
-    let els = this.stack.splice(this.stack.length - m);
-    let node = this.LoadChild(path);
-    node.replaceWith(...els);
-  }
-  LoadTemplate(name, index, id) {
-    let node = this.templates[name][index].cloneNode(true);
-    this.nodes[id] = node;
-    this.stack.push(node);
-  }
-  handleEdit(edit) {
-    switch (edit.type) {
-      case "AppendChildren":
-        this.AppendChildren(edit.m);
-        break;
-      case "AssignId":
-        this.AssignId(edit.path, edit.id);
-        break;
-      case "CreatePlaceholder":
-        this.CreatePlaceholder(edit.id);
-        break;
-      case "CreateTextNode":
-        this.CreateTextNode(edit.value, edit.id);
-        break;
-      case "HydrateText":
-        this.HydrateText(edit.path, edit.value, edit.id);
-        break;
-      case "LoadTemplate":
-        this.LoadTemplate(edit.name, edit.index, edit.id);
-        break;
-      case "PushRoot":
-        this.PushRoot(edit.id);
-        break;
-      case "ReplaceWith":
-        this.ReplaceWith(edit.id, edit.m);
-        break;
-      case "ReplacePlaceholder":
-        this.ReplacePlaceholder(edit.path, edit.m);
-        break;
-      case "InsertAfter":
-        this.InsertAfter(edit.id, edit.m);
-        break;
-      case "InsertBefore":
-        this.InsertBefore(edit.id, edit.m);
-        break;
-      case "Remove":
-        this.Remove(edit.id);
-        break;
-      case "SetText":
-        this.SetText(edit.id, edit.value);
-        break;
-      case "SetAttribute":
-        this.SetAttribute(edit.id, edit.name, edit.value, edit.ns);
-        break;
-      case "RemoveAttribute":
-        this.RemoveAttribute(edit.id, edit.name, edit.ns);
-        break;
-      case "RemoveEventListener":
-        this.RemoveEventListener(edit.id, edit.name);
-        break;
-      case "NewEventListener":
-        let bubbles = event_bubbles(edit.name);
-
-        // if this is a mounted listener, we send the event immediately
-        if (edit.name === "mounted") {
-          window.ipc.postMessage(
-            serializeIpcMessage("user_event", {
-              name: edit.name,
-              element: edit.id,
-              data: null,
-              bubbles,
-            })
-          );
-        } else {
-          this.NewEventListener(edit.name, edit.id, bubbles, (event) => {
-            handler(event, edit.name, bubbles, this.config);
-          });
-        }
-        break;
-    }
-  }
-}
-
 // this handler is only provided on the desktop and liveview implementations since this
 // method is not used by the web implementation
 function handler(event, name, bubbles, config) {
@@ -444,7 +65,44 @@ function handler(event, name, bubbles, config) {
 
     let contents = serialize_event(event);
 
-    /*POST_EVENT_SERIALIZATION*/
+    // TODO: this should be liveview only
+    if (
+      target.tagName === "INPUT" &&
+      (event.type === "change" || event.type === "input")
+    ) {
+      const type = target.getAttribute("type");
+      if (type === "file") {
+        async function read_files() {
+          const files = target.files;
+          const 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())
+            );
+          }
+          let file_engine = {
+            files: file_contents,
+          };
+          contents.files = file_engine;
+
+          if (realId === null) {
+            return;
+          }
+          const message = serializeIpcMessage("user_event", {
+            name: name,
+            element: parseInt(realId),
+            data: contents,
+            bubbles,
+          });
+          window.ipc.postMessage(message);
+        }
+        read_files();
+        return;
+      }
+    }
 
     if (
       target.tagName === "FORM" &&
@@ -506,6 +164,239 @@ function find_real_id(target) {
   return realId;
 }
 
+class ListenerMap {
+  constructor(root) {
+    // bubbling events can listen at the root element
+    this.global = {};
+    // non bubbling events listen at the element the listener was created at
+    this.local = {};
+    this.root = null;
+  }
+
+  create(event_name, element, bubbles, handler) {
+    if (bubbles) {
+      if (this.global[event_name] === undefined) {
+        this.global[event_name] = {};
+        this.global[event_name].active = 1;
+        this.root.addEventListener(event_name, handler);
+      } else {
+        this.global[event_name].active++;
+      }
+    }
+    else {
+      const id = element.getAttribute("data-dioxus-id");
+      if (!this.local[id]) {
+        this.local[id] = {};
+      }
+      element.addEventListener(event_name, handler);
+    }
+  }
+
+  remove(element, event_name, bubbles) {
+    if (bubbles) {
+      this.global[event_name].active--;
+      if (this.global[event_name].active === 0) {
+        this.root.removeEventListener(event_name, this.global[event_name].callback);
+        delete this.global[event_name];
+      }
+    }
+    else {
+      const id = element.getAttribute("data-dioxus-id");
+      delete this.local[id][event_name];
+      if (this.local[id].length === 0) {
+        delete this.local[id];
+      }
+      element.removeEventListener(event_name, this.global[event_name].callback);
+    }
+  }
+
+  removeAllNonBubbling(element) {
+    const id = element.getAttribute("data-dioxus-id");
+    delete this.local[id];
+  }
+}
+function SetAttributeInner(node, field, value, ns) {
+  const name = field;
+  if (ns === "style") {
+    // ????? why do we need to do this
+    if (node.style === undefined) {
+      node.style = {};
+    }
+    node.style[name] = value;
+  } else if (ns !== null && ns !== undefined && ns !== "") {
+    node.setAttributeNS(ns, name, value);
+  } else {
+    switch (name) {
+      case "value":
+        if (value !== node.value) {
+          node.value = value;
+        }
+        break;
+      case "initial_value":
+        node.defaultValue = value;
+        break;
+      case "checked":
+        node.checked = truthy(value);
+        break;
+      case "selected":
+        node.selected = truthy(value);
+        break;
+      case "dangerous_inner_html":
+        node.innerHTML = value;
+        break;
+      default:
+        // https://github.com/facebook/react/blob/8b88ac2592c5f555f315f9440cbb665dd1e7457a/packages/react-dom/src/shared/DOMProperty.js#L352-L364
+        if (!truthy(value) && bool_attrs.hasOwnProperty(name)) {
+          node.removeAttribute(name);
+        } else {
+          node.setAttribute(name, value);
+        }
+    }
+  }
+}
+function LoadChild(array) {
+  // iterate through each number and get that child
+  node = stack[stack.length - 1];
+
+  for (let i = 0; i < array.length; i++) {
+    end = array[i];
+    for (node = node.firstChild; end > 0; end--) {
+      node = node.nextSibling;
+    }
+  }
+  return node;
+}
+const listeners = new ListenerMap();
+let nodes = [];
+let stack = [];
+let root;
+const templates = {};
+let node, els, end, k;
+function initialize(root) {
+  nodes = [root];
+  stack = [root];
+  listeners.root = root;
+}
+function AppendChildren(id, many) {
+  root = nodes[id];
+  els = stack.splice(stack.length - many);
+  for (k = 0; k < many; k++) {
+    root.appendChild(els[k]);
+  }
+}
+const bool_attrs = {
+  allowfullscreen: true,
+  allowpaymentrequest: true,
+  async: true,
+  autofocus: true,
+  autoplay: true,
+  checked: true,
+  controls: true,
+  default: true,
+  defer: true,
+  disabled: true,
+  formnovalidate: true,
+  hidden: true,
+  ismap: true,
+  itemscope: true,
+  loop: true,
+  multiple: true,
+  muted: true,
+  nomodule: true,
+  novalidate: true,
+  open: true,
+  playsinline: true,
+  readonly: true,
+  required: true,
+  reversed: true,
+  selected: true,
+  truespeed: true,
+  webkitdirectory: true,
+};
+function truthy(val) {
+  return val === "true" || val === true;
+}
+
+
+function getClientRect(id) {
+  const node = nodes[id];
+  if (!node) {
+    return;
+  }
+  const rect = node.getBoundingClientRect();
+  return {
+    type: "GetClientRect",
+    origin: [rect.x, rect.y],
+    size: [rect.width, rect.height],
+  };
+}
+
+function scrollTo(id, behavior) {
+  const node = nodes[id];
+  if (!node) {
+    return false;
+  }
+  node.scrollIntoView({
+    behavior: behavior,
+  });
+  return true;
+}
+
+/// Set the focus on the element
+function setFocus(id, focus) {
+  const node = nodes[id];
+  if (!node) {
+    return false;
+  }
+  if (focus) {
+    node.focus();
+  } else {
+    node.blur();
+  }
+  return true;
+}
+
+function saveTemplate(template) {
+  let roots = [];
+  for (let root of template.roots) {
+    roots.push(this.MakeTemplateNode(root));
+  }
+  this.templates[template.name] = roots;
+}
+
+function makeTemplateNode(node) {
+  switch (node.type) {
+    case "Text":
+      return document.createTextNode(node.text);
+    case "Dynamic":
+      let dyn = document.createElement("pre");
+      dyn.hidden = true;
+      return dyn;
+    case "DynamicText":
+      return document.createTextNode("placeholder");
+    case "Element":
+      let el;
+
+      if (node.namespace != null) {
+        el = document.createElementNS(node.namespace, node.tag);
+      } else {
+        el = document.createElement(node.tag);
+      }
+
+      for (let attr of node.attrs) {
+        if (attr.type == "Static") {
+          setAttributeInner(el, attr.name, attr.value, attr.namespace);
+        }
+      }
+
+      for (let child of node.children) {
+        el.appendChild(this.MakeTemplateNode(child));
+      }
+
+      return el;
+  }
+}
+
 function get_mouse_data(event) {
   const {
     altKey,

+ 1 - 7
packages/interpreter/src/lib.rs

@@ -6,14 +6,8 @@ mod sledgehammer_bindings;
 #[cfg(feature = "sledgehammer")]
 pub use sledgehammer_bindings::*;
 
-#[cfg(feature = "web")]
-mod bindings;
-
-#[cfg(feature = "web")]
-pub use bindings::Interpreter;
-
 // Common bindings for minimal usage.
-#[cfg(feature = "minimal_bindings")]
+#[cfg(all(feature = "minimal_bindings", feature = "web"))]
 pub mod minimal_bindings {
     use wasm_bindgen::{prelude::wasm_bindgen, JsValue};
     #[wasm_bindgen(module = "/src/common.js")]

+ 163 - 10
packages/interpreter/src/sledgehammer_bindings.rs

@@ -1,8 +1,12 @@
+#[cfg(feature = "web")]
 use js_sys::Function;
 use sledgehammer_bindgen::bindgen;
+#[cfg(feature = "web")]
 use web_sys::Node;
 
-#[bindgen]
+pub const SLEDGEHAMMER_JS: &str = GENERATED_JS;
+#[cfg(feature = "web")]
+#[bindgen(module)]
 mod js {
     const JS: &str = r#"
     class ListenerMap {
@@ -123,7 +127,7 @@ mod js {
     export function get_node(id) {
         return nodes[id];
     }
-    export function initilize(root, handler) {
+    export function initialize(root, handler) {
         listeners.handler = handler;
         nodes = [root];
         stack = [root];
@@ -172,7 +176,7 @@ mod js {
 
     extern "C" {
         #[wasm_bindgen]
-        pub fn save_template(nodes: Vec<Node>, tmpl_id: u32);
+        pub fn save_template(nodes: Vec<Node>, tmpl_id: u16);
 
         #[wasm_bindgen]
         pub fn set_node(id: u32, node: Node);
@@ -181,7 +185,7 @@ mod js {
         pub fn get_node(id: u32) -> Node;
 
         #[wasm_bindgen]
-        pub fn initilize(root: Node, handler: &Function);
+        pub fn initialize(root: Node, handler: &Function);
     }
 
     fn mount_to_root() {
@@ -190,19 +194,19 @@ mod js {
     fn push_root(root: u32) {
         "{stack.push(nodes[$root$]);}"
     }
-    fn append_children(id: u32, many: u32) {
+    fn append_children(id: u32, many: u16) {
         "{AppendChildren($id$, $many$);}"
     }
     fn pop_root() {
         "{stack.pop();}"
     }
-    fn replace_with(id: u32, n: u32) {
+    fn replace_with(id: u32, n: u16) {
         "{root = nodes[$id$]; els = stack.splice(stack.length-$n$); if (root.listening) { listeners.removeAllNonBubbling(root); } root.replaceWith(...els);}"
     }
-    fn insert_after(id: u32, n: u32) {
+    fn insert_after(id: u32, n: u16) {
         "{nodes[$id$].after(...stack.splice(stack.length-$n$));}"
     }
-    fn insert_before(id: u32, n: u32) {
+    fn insert_before(id: u32, n: u16) {
         "{nodes[$id$].before(...stack.splice(stack.length-$n$));}"
     }
     fn remove(id: u32) {
@@ -273,10 +277,159 @@ mod js {
             nodes[$id$] = node;
         }"#
     }
-    fn replace_placeholder(ptr: u32, len: u8, n: u32) {
+    fn replace_placeholder(ptr: u32, len: u8, n: u16) {
         "{els = stack.splice(stack.length - $n$); node = LoadChild($ptr$, $len$); node.replaceWith(...els);}"
     }
-    fn load_template(tmpl_id: u32, index: u32, id: u32) {
+    fn load_template(tmpl_id: u16, index: u16, id: u32) {
         "{node = templates[$tmpl_id$][$index$].cloneNode(true); nodes[$id$] = node; stack.push(node);}"
     }
 }
+
+#[cfg(not(feature = "web"))]
+#[bindgen]
+mod js {
+    const JS_FILE: &str = "./src/interpreter.js";
+
+    fn mount_to_root() {
+        "{AppendChildren(root, stack.length-1);}"
+    }
+    fn push_root(root: u32) {
+        "{stack.push(nodes[$root$]);}"
+    }
+    fn append_children(id: u32, many: u16) {
+        "{AppendChildren($id$, $many$);}"
+    }
+    fn append_children_to_top(many: u16) {
+        "{
+            root = stack[stack.length-many-1];
+            els = stack.splice(stack.length-many);
+            for (k = 0; k < many; k++) {
+                root.appendChild(els[k]);
+            }
+        }"
+    }
+    fn pop_root() {
+        "{stack.pop();}"
+    }
+    fn replace_with(id: u32, n: u16) {
+        "{root = nodes[$id$]; els = stack.splice(stack.length-$n$); if (root.listening) { listeners.removeAllNonBubbling(root); } root.replaceWith(...els);}"
+    }
+    fn insert_after(id: u32, n: u16) {
+        "{nodes[$id$].after(...stack.splice(stack.length-$n$));}"
+    }
+    fn insert_before(id: u32, n: u16) {
+        "{nodes[$id$].before(...stack.splice(stack.length-$n$));}"
+    }
+    fn remove(id: u32) {
+        "{node = nodes[$id$]; if (node !== undefined) { if (node.listening) { listeners.removeAllNonBubbling(node); } node.remove(); }}"
+    }
+    fn create_raw_text(text: &str) {
+        "{stack.push(document.createTextNode($text$));}"
+    }
+    fn create_text_node(text: &str, id: u32) {
+        "{node = document.createTextNode($text$); nodes[$id$] = node; stack.push(node);}"
+    }
+    fn create_element(element: &str<u8, el>) {
+        "{stack.push(document.createElement($element$))}"
+    }
+    fn create_element_ns(element: &str<u8, el>, ns: &str<u8, namespace>) {
+        "{stack.push(document.createElementNS($ns$, $element$))}"
+    }
+    fn create_placeholder(id: u32) {
+        "{node = document.createElement('pre'); node.hidden = true; stack.push(node); nodes[$id$] = node;}"
+    }
+    fn add_placeholder() {
+        "{node = document.createElement('pre'); node.hidden = true; stack.push(node);}"
+    }
+    fn new_event_listener(event_name: &str<u8, evt>, id: u32, bubbles: u8) {
+        r#"
+        node = nodes[id];
+        if(node.listening){
+            node.listening += 1;
+        } else {
+            node.listening = 1;
+        }
+        node.setAttribute('data-dioxus-id', `\${id}`);
+
+        // if this is a mounted listener, we send the event immediately
+        if (edit.name === "mounted") {
+            window.ipc.postMessage(
+                serializeIpcMessage("user_event", {
+                    name: edit.name,
+                    element: edit.id,
+                    data: null,
+                    bubbles,
+                })
+            );
+        } else {
+            listeners.create(event_name, node, bubbles, (event) => {
+                handler(event, event_name, bubbles, config);
+            });
+        }"#
+    }
+    fn remove_event_listener(event_name: &str<u8, evt>, id: u32, bubbles: u8) {
+        "{node = nodes[$id$]; node.listening -= 1; node.removeAttribute('data-dioxus-id'); listeners.remove(node, $event_name$, $bubbles$);}"
+    }
+    fn set_text(id: u32, text: &str) {
+        "{nodes[$id$].textContent = $text$;}"
+    }
+    fn set_attribute(id: u32, field: &str<u8, attr>, value: &str, ns: &str<u8, ns_cache>) {
+        "{node = nodes[$id$]; SetAttributeInner(node, $field$, $value$, $ns$);}"
+    }
+    fn set_top_attribute(field: &str<u8, attr>, value: &str, ns: &str<u8, ns_cache>) {
+        "{SetAttributeInner(stack[stack.length-1], $field$, $value$, $ns$);}"
+    }
+    fn remove_attribute(id: u32, field: &str<u8, attr>, ns: &str<u8, ns_cache>) {
+        r#"{
+            node = nodes[$id$];
+            if (!ns) {
+                switch (field) {
+                    case "value":
+                        node.value = "";
+                        break;
+                    case "checked":
+                        node.checked = false;
+                        break;
+                    case "selected":
+                        node.selected = false;
+                        break;
+                    case "dangerous_inner_html":
+                        node.innerHTML = "";
+                        break;
+                    default:
+                        node.removeAttribute(field);
+                        break;
+                }
+            } else if (ns == "style") {
+                node.style.removeProperty(name);
+            } else {
+                node.removeAttributeNS(ns, field);
+            }
+        }"#
+    }
+    fn assign_id(array: &[u8], id: u32) {
+        "{nodes[$id$] = LoadChild($array$);}"
+    }
+    fn hydrate_text(array: &[u8], value: &str, id: u32) {
+        r#"{
+            node = LoadChild($array$);
+            if (node.nodeType == Node.TEXT_NODE) {
+                node.textContent = value;
+            } else {
+                let text = document.createTextNode(value);
+                node.replaceWith(text);
+                node = text;
+            }
+            nodes[$id$] = node;
+        }"#
+    }
+    fn replace_placeholder(array: &[u8], n: u16) {
+        "{els = stack.splice(stack.length - $n$); node = LoadChild($array$); node.replaceWith(...els);}"
+    }
+    fn load_template(tmpl_id: u16, index: u16, id: u32) {
+        "{node = templates[$tmpl_id$][$index$].cloneNode(true); nodes[$id$] = node; stack.push(node);}"
+    }
+    fn add_templates(tmpl_id: u16, len: u16) {
+        "{templates[$tmpl_id$] = stack.splice(stack.length-$len$);}"
+    }
+}

+ 1 - 0
packages/liveview/.gitignore

@@ -0,0 +1 @@
+/src/minified.js

+ 10 - 1
packages/liveview/Cargo.toml

@@ -22,9 +22,10 @@ tokio-stream = { version = "0.1.11", features = ["net"] }
 tokio-util = { version = "0.7.4", features = ["rt"] }
 serde = { version = "1.0.151", features = ["derive"] }
 serde_json = "1.0.91"
+rustc-hash = { workspace = true }
 dioxus-html = { workspace = true, features = ["serialize"] }
 dioxus-core = { workspace = true, features = ["serialize"] }
-dioxus-interpreter-js = { workspace = true }
+dioxus-interpreter-js = { workspace = true, features = ["sledgehammer"] }
 dioxus-hot-reload = { workspace = true, optional = true }
 
 # warp
@@ -52,6 +53,10 @@ axum = { version = "0.6.1", features = ["ws"] }
 salvo = { version = "0.44.1", features = ["affix", "ws"] }
 tower = "0.4.13"
 
+[build-dependencies]
+dioxus-interpreter-js = { workspace = true, features = ["sledgehammer"] }
+minify-js = "0.5.6"
+
 [features]
 default = ["hot-reload"]
 # actix = ["actix-files", "actix-web", "actix-ws"]
@@ -61,6 +66,10 @@ hot-reload = ["dioxus-hot-reload"]
 name = "axum"
 required-features = ["axum"]
 
+[[example]]
+name = "axum_stress"
+required-features = ["axum"]
+
 [[example]]
 name = "salvo"
 required-features = ["salvo"]

+ 63 - 0
packages/liveview/build.rs

@@ -0,0 +1,63 @@
+use dioxus_interpreter_js::COMMON_JS;
+use dioxus_interpreter_js::SLEDGEHAMMER_JS;
+use minify_js::*;
+
+fn main() {
+    let serialize_file_uploads = r#"if (
+            target.tagName === "INPUT" &&
+            (event.type === "change" || event.type === "input")
+          ) {
+            const type = target.getAttribute("type");
+            if (type === "file") {
+              async function read_files() {
+                const files = target.files;
+                const 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())
+                  );
+                }
+                let file_engine = {
+                  files: file_contents,
+                };
+                contents.files = file_engine;
+      
+                if (realId === null) {
+                  return;
+                }
+                const message = serializeIpcMessage("user_event", {
+                  name: name,
+                  element: parseInt(realId),
+                  data: contents,
+                  bubbles,
+                });
+                window.ipc.postMessage(message);
+              }
+              read_files();
+              return;
+            }
+          }"#;
+    let mut interpreter =
+        SLEDGEHAMMER_JS.replace("/*POST_EVENT_SERIALIZATION*/", serialize_file_uploads);
+    while let Some(import_start) = interpreter.find("import") {
+        let import_end = interpreter[import_start..]
+            .find(|c| c == ';' || c == '\n')
+            .map(|i| i + import_start)
+            .unwrap_or_else(|| interpreter.len());
+        interpreter.replace_range(import_start..import_end, "");
+    }
+
+    let main_js = std::fs::read_to_string("src/main.js").unwrap();
+
+    let js = format!("{interpreter}\n{main_js}");
+    // std::fs::write("src/minified.js", &js).unwrap();
+
+    let session = Session::new();
+    let mut out = Vec::new();
+    minify(&session, TopLevelMode::Module, js.as_bytes(), &mut out).unwrap();
+    let minified = String::from_utf8(out).unwrap();
+    std::fs::write("src/minified.js", minified).unwrap();
+}

+ 65 - 0
packages/liveview/examples/axum_stress.rs

@@ -0,0 +1,65 @@
+use axum::{extract::ws::WebSocketUpgrade, response::Html, routing::get, Router};
+use dioxus::prelude::*;
+
+fn app(cx: Scope) -> Element {
+    let state = use_state(cx, || 0);
+    use_future(cx, (), |_| {
+        to_owned![state];
+        async move {
+            loop {
+                state += 1;
+                tokio::time::sleep(std::time::Duration::from_millis(1)).await;
+            }
+        }
+    });
+
+    cx.render(rsx! {
+        for _ in 0..10000 {
+            div {
+                "hello axum! {state}"
+            }
+        }
+    })
+}
+
+#[tokio::main]
+async fn main() {
+    pretty_env_logger::init();
+
+    let addr: std::net::SocketAddr = ([127, 0, 0, 1], 3030).into();
+
+    let view = dioxus_liveview::LiveViewPool::new();
+
+    let app = Router::new()
+        .route(
+            "/",
+            get(move || async move {
+                Html(format!(
+                    r#"
+            <!DOCTYPE html>
+            <html>
+                <head> <title>Dioxus LiveView with axum</title>  </head>
+                <body> <div id="main"></div> </body>
+                {glue}
+            </html>
+            "#,
+                    glue = dioxus_liveview::interpreter_glue(&format!("ws://{addr}/ws"))
+                ))
+            }),
+        )
+        .route(
+            "/ws",
+            get(move |ws: WebSocketUpgrade| async move {
+                ws.on_upgrade(move |socket| async move {
+                    _ = view.launch(dioxus_liveview::axum_socket(socket), app).await;
+                })
+            }),
+        );
+
+    println!("Listening on http://{addr}");
+
+    axum::Server::bind(&addr.to_string().parse().unwrap())
+        .serve(app.into_make_service())
+        .await
+        .unwrap();
+}

+ 1 - 1
packages/liveview/src/adapters/axum_adapter.rs

@@ -20,5 +20,5 @@ fn transform_rx(message: Result<Message, axum::Error>) -> Result<Vec<u8>, LiveVi
 }
 
 async fn transform_tx(message: Vec<u8>) -> Result<Message, axum::Error> {
-    Ok(Message::Text(String::from_utf8_lossy(&message).to_string()))
+    Ok(Message::Binary(message))
 }

+ 2 - 58
packages/liveview/src/lib.rs

@@ -37,74 +37,18 @@ pub enum LiveViewError {
     SendingFailed,
 }
 
-use once_cell::sync::Lazy;
-
-static INTERPRETER_JS: Lazy<String> = Lazy::new(|| {
-    let interpreter = dioxus_interpreter_js::INTERPRETER_JS;
-    let serialize_file_uploads = r#"if (
-      target.tagName === "INPUT" &&
-      (event.type === "change" || event.type === "input")
-    ) {
-      const type = target.getAttribute("type");
-      if (type === "file") {
-        async function read_files() {
-          const files = target.files;
-          const 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())
-            );
-          }
-          let file_engine = {
-            files: file_contents,
-          };
-          contents.files = file_engine;
-
-          if (realId === null) {
-            return;
-          }
-          const message = serializeIpcMessage("user_event", {
-            name: name,
-            element: parseInt(realId),
-            data: contents,
-            bubbles,
-          });
-          window.ipc.postMessage(message);
-        }
-        read_files();
-        return;
-      }
-    }"#;
-
-    let interpreter = interpreter.replace("/*POST_EVENT_SERIALIZATION*/", serialize_file_uploads);
-    interpreter.replace("import { setAttributeInner } from \"./common.js\";", "")
-});
-
-static COMMON_JS: Lazy<String> = Lazy::new(|| {
-    let common = dioxus_interpreter_js::COMMON_JS;
-    common.replace("export", "")
-});
-
-static MAIN_JS: &str = include_str!("./main.js");
+static MINIFIED: &str = include_str!("./minified.js");
 
 /// This script that gets injected into your app connects this page to the websocket endpoint
 ///
 /// Once the endpoint is connected, it will send the initial state of the app, and then start
 /// processing user events and returning edits to the liveview instance
 pub fn interpreter_glue(url: &str) -> String {
-    let js = &*INTERPRETER_JS;
-    let common = &*COMMON_JS;
     format!(
         r#"
 <script>
     var WS_ADDR = "{url}";
-    {js}
-    {common}
-    {MAIN_JS}
-    main();
+    {MINIFIED}
 </script>
     "#
     )

+ 22 - 15
packages/liveview/src/main.js

@@ -7,10 +7,9 @@ function main() {
 
 class IPC {
   constructor(root) {
-    // connect to the websocket
-    window.interpreter = new Interpreter(root, new InterpreterConfig(false));
-
-    let ws = new WebSocket(WS_ADDR);
+    initialize(root);
+    const ws = new WebSocket(WS_ADDR);
+    ws.binaryType = "arraybuffer";
 
     function ping() {
       ws.send("__ping__");
@@ -27,17 +26,23 @@ class IPC {
     };
 
     ws.onmessage = (message) => {
-      // Ignore pongs
-      if (message.data != "__pong__") {
-        const event = JSON.parse(message.data);
-        switch (event.type) {
-          case "edits":
-            let edits = event.data;
-            window.interpreter.handleEdits(edits);
-            break;
-          case "query":
-            Function("Eval", `"use strict";${event.data};`)();
-            break;
+      if (message.data instanceof ArrayBuffer) {
+        // binary frame
+        run_from_bytes(message.data);
+      } else {
+        // text frame
+        // Ignore pongs
+        if (message.data != "__pong__") {
+          const event = JSON.parse(message.data);
+          switch (event.type) {
+            case "edits":
+              let edits = event.data;
+              window.interpreter.handleEdits(edits);
+              break;
+            case "query":
+              Function("Eval", `"use strict";${event.data};`)();
+              break;
+          }
         }
       }
     };
@@ -49,3 +54,5 @@ class IPC {
     this.ws.send(msg);
   }
 }
+
+main();

+ 159 - 16
packages/liveview/src/pool.rs

@@ -4,9 +4,11 @@ use crate::{
     query::{QueryEngine, QueryResult},
     LiveViewError,
 };
-use dioxus_core::{prelude::*, Mutations};
-use dioxus_html::{EventData, HtmlEvent, MountedData};
+use dioxus_core::{prelude::*, BorrowedAttributeValue, Mutations};
+use dioxus_html::{event_bubbles, EventData, HtmlEvent, MountedData};
+use dioxus_interpreter_js::Channel;
 use futures_util::{pin_mut, SinkExt, StreamExt};
+use rustc_hash::FxHashMap;
 use serde::Serialize;
 use std::{rc::Rc, time::Duration};
 use tokio_util::task::LocalPoolHandle;
@@ -107,7 +109,7 @@ impl<S> LiveViewSocket for S where
 ///
 /// This function makes it easy to integrate Dioxus LiveView with any socket-based framework.
 ///
-/// As long as your framework can provide a Sink and Stream of Strings, you can use this function.
+/// As long as your framework can provide a Sink and Stream of Bytes, you can use this function.
 ///
 /// You might need to transform the error types of the web backend into the LiveView error type.
 pub async fn run(mut vdom: VirtualDom, ws: impl LiveViewSocket) -> Result<(), LiveViewError> {
@@ -120,20 +122,31 @@ pub async fn run(mut vdom: VirtualDom, ws: impl LiveViewSocket) -> Result<(), Li
         rx
     };
 
+    let mut templates: FxHashMap<String, u16> = Default::default();
+    let mut max_template_count = 0;
+
     // Create the a proxy for query engine
     let (query_tx, mut query_rx) = tokio::sync::mpsc::unbounded_channel();
     let query_engine = QueryEngine::new(query_tx);
     vdom.base_scope().provide_context(query_engine.clone());
     init_eval(vdom.base_scope());
 
-    // todo: use an efficient binary packed format for this
-    let edits = serde_json::to_string(&ClientUpdate::Edits(vdom.rebuild())).unwrap();
-
     // pin the futures so we can use select!
     pin_mut!(ws);
 
-    // send the initial render to the client
-    ws.send(edits.into_bytes()).await?;
+    let mut edit_channel = Channel::default();
+    if let Some(edits) = {
+        let mutations = vdom.rebuild();
+        apply_edits(
+            mutations,
+            &mut edit_channel,
+            &mut templates,
+            &mut max_template_count,
+        )
+    } {
+        // send the initial render to the client
+        ws.send(edits).await?;
+    }
 
     // desktop uses this wrapper struct thing around the actual event itself
     // this is sorta driven by tao/wry
@@ -160,7 +173,7 @@ pub async fn run(mut vdom: VirtualDom, ws: impl LiveViewSocket) -> Result<(), Li
                 match evt.as_ref().map(|o| o.as_deref()) {
                     // respond with a pong every ping to keep the websocket alive
                     Some(Ok(b"__ping__")) => {
-                        ws.send(b"__pong__".to_vec()).await?;
+                        // ws.send(b"__pong__".to_vec()).await?;
                     }
                     Some(Ok(evt)) => {
                         if let Ok(message) = serde_json::from_str::<IpcMessage>(&String::from_utf8_lossy(evt)) {
@@ -199,7 +212,7 @@ pub async fn run(mut vdom: VirtualDom, ws: impl LiveViewSocket) -> Result<(), Li
 
             // handle any new queries
             Some(query) = query_rx.recv() => {
-                ws.send(serde_json::to_string(&ClientUpdate::Query(query)).unwrap().into_bytes()).await?;
+                // ws.send(serde_json::to_string(&ClientUpdate::Query(query)).unwrap().into_bytes()).await?;
             }
 
             Some(msg) = hot_reload_wait => {
@@ -221,13 +234,143 @@ pub async fn run(mut vdom: VirtualDom, ws: impl LiveViewSocket) -> Result<(), Li
             .render_with_deadline(tokio::time::sleep(Duration::from_millis(10)))
             .await;
 
-        ws.send(
-            serde_json::to_string(&ClientUpdate::Edits(edits))
-                .unwrap()
-                .into_bytes(),
-        )
-        .await?;
+        if let Some(edits) = {
+            apply_edits(
+                edits,
+                &mut edit_channel,
+                &mut templates,
+                &mut max_template_count,
+            )
+        } {
+            ws.send(edits).await?;
+        }
+    }
+}
+
+fn add_template(
+    template: &Template,
+    channel: &mut Channel,
+    templates: &mut FxHashMap<String, u16>,
+    max_template_count: &mut u16,
+) {
+    for (idx, root) in template.roots.iter().enumerate() {
+        create_template_node(channel, root);
+        templates.insert(template.name.to_owned(), *max_template_count);
     }
+    channel.add_templates(*max_template_count, template.roots.len() as u16);
+
+    *max_template_count += 1
+}
+
+fn create_template_node(channel: &mut Channel, v: &TemplateNode) {
+    use TemplateNode::*;
+    match v {
+        Element {
+            tag,
+            namespace,
+            attrs,
+            children,
+            ..
+        } => {
+            // Push the current node onto the stack
+            match namespace {
+                Some(ns) => channel.create_element_ns(tag, ns),
+                None => channel.create_element(tag),
+            }
+            // Set attributes on the current node
+            for attr in *attrs {
+                if let TemplateAttribute::Static {
+                    name,
+                    value,
+                    namespace,
+                } = attr
+                {
+                    channel.set_top_attribute(name, value, namespace.unwrap_or_default())
+                }
+            }
+            // Add each child to the stack
+            for child in *children {
+                create_template_node(channel, child);
+            }
+            // Add all children to the parent
+            channel.append_children_to_top(children.len() as u16);
+        }
+        Text { text } => channel.create_raw_text(text),
+        DynamicText { .. } => channel.create_raw_text("p"),
+        Dynamic { .. } => channel.add_placeholder(),
+    }
+}
+
+fn apply_edits(
+    mutations: Mutations,
+    channel: &mut Channel,
+    templates: &mut FxHashMap<String, u16>,
+    max_template_count: &mut u16,
+) -> Option<Vec<u8>> {
+    use dioxus_core::Mutation::*;
+    if mutations.templates.is_empty() && mutations.edits.is_empty() {
+        return None;
+    }
+    for template in mutations.templates {
+        add_template(&template, channel, templates, max_template_count);
+    }
+    for edit in mutations.edits {
+        match edit {
+            AppendChildren { id, m } => channel.append_children(id.0 as u32, m as u16),
+            AssignId { path, id } => channel.assign_id(path, id.0 as u32),
+            CreatePlaceholder { id } => channel.create_placeholder(id.0 as u32),
+            CreateTextNode { value, id } => channel.create_text_node(value, id.0 as u32),
+            HydrateText { path, value, id } => channel.hydrate_text(path, value, id.0 as u32),
+            LoadTemplate { name, index, id } => {
+                if let Some(tmpl_id) = templates.get(name) {
+                    channel.load_template(*tmpl_id as u16, index as u16, id.0 as u32)
+                }
+            }
+            ReplaceWith { id, m } => channel.replace_with(id.0 as u32, m as u16),
+            ReplacePlaceholder { path, m } => channel.replace_placeholder(path, m as u16),
+            InsertAfter { id, m } => channel.insert_after(id.0 as u32, m as u16),
+            InsertBefore { id, m } => channel.insert_before(id.0 as u32, m as u16),
+            SetAttribute {
+                name,
+                value,
+                id,
+                ns,
+            } => match value {
+                BorrowedAttributeValue::Text(txt) => {
+                    channel.set_attribute(id.0 as u32, name, txt, ns.unwrap_or_default())
+                }
+                BorrowedAttributeValue::Float(f) => {
+                    channel.set_attribute(id.0 as u32, name, &f.to_string(), ns.unwrap_or_default())
+                }
+                BorrowedAttributeValue::Int(n) => {
+                    channel.set_attribute(id.0 as u32, name, &n.to_string(), ns.unwrap_or_default())
+                }
+                BorrowedAttributeValue::Bool(b) => channel.set_attribute(
+                    id.0 as u32,
+                    name,
+                    if b { "true" } else { "false" },
+                    ns.unwrap_or_default(),
+                ),
+                BorrowedAttributeValue::None => {
+                    channel.remove_attribute(id.0 as u32, name, ns.unwrap_or_default())
+                }
+                _ => unreachable!(),
+            },
+            SetText { value, id } => channel.set_text(id.0 as u32, value),
+            NewEventListener { name, id, .. } => {
+                channel.new_event_listener(name, id.0 as u32, event_bubbles(name) as u8)
+            }
+            RemoveEventListener { name, id } => {
+                channel.remove_event_listener(name, id.0 as u32, event_bubbles(name) as u8)
+            }
+            Remove { id } => channel.remove(id.0 as u32),
+            PushRoot { id } => channel.push_root(id.0 as u32),
+        }
+    }
+
+    let bytes: Vec<_> = channel.export_memory().collect();
+    channel.reset();
+    Some(bytes)
 }
 
 #[derive(Serialize)]

+ 1 - 0
packages/web/Cargo.toml

@@ -15,6 +15,7 @@ dioxus-html = { workspace = true, features = ["wasm-bind"] }
 dioxus-interpreter-js = { workspace = true, features = [
     "sledgehammer",
     "minimal_bindings",
+    "web",
 ] }
 
 js-sys = "0.3.56"

+ 9 - 9
packages/web/src/dom.rs

@@ -25,8 +25,8 @@ pub struct WebsysDom {
     document: Document,
     #[allow(dead_code)]
     pub(crate) root: Element,
-    templates: FxHashMap<String, u32>,
-    max_template_id: u32,
+    templates: FxHashMap<String, u16>,
+    max_template_id: u16,
     pub(crate) interpreter: Channel,
     event_channel: mpsc::UnboundedSender<UiEvent>,
 }
@@ -90,7 +90,7 @@ impl WebsysDom {
             }
         }));
 
-        dioxus_interpreter_js::initilize(
+        dioxus_interpreter_js::initialize(
             root.clone().unchecked_into(),
             handler.as_ref().unchecked_ref(),
         );
@@ -175,7 +175,7 @@ impl WebsysDom {
         let mut to_mount = Vec::new();
         for edit in &edits {
             match edit {
-                AppendChildren { id, m } => i.append_children(id.0 as u32, *m as u32),
+                AppendChildren { id, m } => i.append_children(id.0 as u32, *m as u16),
                 AssignId { path, id } => {
                     i.assign_id(path.as_ptr() as u32, path.len() as u8, id.0 as u32)
                 }
@@ -186,15 +186,15 @@ impl WebsysDom {
                 }
                 LoadTemplate { name, index, id } => {
                     if let Some(tmpl_id) = self.templates.get(*name) {
-                        i.load_template(*tmpl_id, *index as u32, id.0 as u32)
+                        i.load_template(*tmpl_id, *index as u16, id.0 as u32)
                     }
                 }
-                ReplaceWith { id, m } => i.replace_with(id.0 as u32, *m as u32),
+                ReplaceWith { id, m } => i.replace_with(id.0 as u32, *m as u16),
                 ReplacePlaceholder { path, m } => {
-                    i.replace_placeholder(path.as_ptr() as u32, path.len() as u8, *m as u32)
+                    i.replace_placeholder(path.as_ptr() as u32, path.len() as u8, *m as u16)
                 }
-                InsertAfter { id, m } => i.insert_after(id.0 as u32, *m as u32),
-                InsertBefore { id, m } => i.insert_before(id.0 as u32, *m as u32),
+                InsertAfter { id, m } => i.insert_after(id.0 as u32, *m as u16),
+                InsertBefore { id, m } => i.insert_before(id.0 as u32, *m as u16),
                 SetAttribute {
                     name,
                     value,

+ 1 - 0
packages/web/src/lib.rs

@@ -54,6 +54,7 @@
 //     - Do DOM work in the next requestAnimationFrame callback
 
 pub use crate::cfg::Config;
+#[cfg(feature = "file_engine")]
 pub use crate::file_engine::WebFileEngineExt;
 use dioxus_core::{Element, Scope, VirtualDom};
 use futures_util::{