uhtml: This sequence of renders crashes in both V3 and V4

I’m monkey testing my app and have uncovered a sequence of renders that will crash both V3 and V4. I apologize for the complexity of this example; this is the shortest sequence of renders that I have found that causes it.

The error is

DOMException: Failed to execute 'setStartAfter' on 'Range': the given Node has no parent.
at range_default (http://localhost:5173/node_modules/.vite/deps/uhtml.js?v=3ae075b9:68:11)
at remove (http://localhost:5173/node_modules/.vite/deps/uhtml.js?v=3ae075b9:77:55)
at DocumentFragment.replaceWith (http://localhost:5173/node_modules/.vite/deps/uhtml.js?v=3ae075b9:106:5)
at Object.hole (http://localhost:5173/node_modules/.vite/deps/uhtml.js?v=3ae075b9:271:9)
at unroll (http://localhost:5173/node_modules/.vite/deps/uhtml.js?v=3ae075b9:425:21)
at unrollValues (http://localhost:5173/node_modules/.vite/deps/uhtml.js?v=3ae075b9:436:19)
at unroll (http://localhost:5173/node_modules/.vite/deps/uhtml.js?v=3ae075b9:411:18)
at unrollValues (http://localhost:5173/node_modules/.vite/deps/uhtml.js?v=3ae075b9:436:19)
at unrollValues (http://localhost:5173/node_modules/.vite/deps/uhtml.js?v=3ae075b9:438:7)
at unroll (http://localhost:5173/node_modules/.vite/deps/uhtml.js?v=3ae075b9:411:18)

start.js

import { html, render } from "uhtml";
import { data } from "./data.js";

/**
 * This tree is a very simplified version of my internal data structure
 * @typedef {Object} TreeBase
 * @property {string} className
 * @property {Object} props
 * @property {TreeBase[]} children
 * @property {string} id
 *
 * @typedef {import("uhtml").Hole} Hole
 */

/**
 * Wrap the code for a node into a component
 * @param {TreeBase} node
 * @param {Hole} body
 * @returns {Hole}
 */
function component(node, body) {
  return html`<div class=${node.className} id=${node.id} style=${""}>
    ${body}
  </div>`;
}

/**
 * A Stack has mulitple children
 * @param {TreeBase} node
 * @returns {Hole}
 */
function Stack(node) {
  return component(
    node,
    html`${node.children.map((child) => html`<div>${content(child)}</div>`)}`,
  );
}

/**
 * A Gap is simply a spacer
 * @param {TreeBase} node
 * @returns {Hole}
 */
function Gap(node) {
  return component(node, html`<div />`);
}

/**
 * Invoke the correct template for each node
 * @param {Object} node
 * @returns {Hole}
 */
function content(node) {
  if (node.className == "Gap") return Gap(node);
  else if (node.className == "Stack") return Stack(node);
  throw new Error("should not happen");
}

// cycle through the trees

let index = 0;
let step = 1;

function rep() {
  console.log({ index });
  try {
    render(document.body, content(data[index]));
  } catch (e) {
    console.error(e);
    clearInterval(timer);
  }
  index += step;
  index = index % data.length;
}
const timer = setInterval(rep, 100);

data.js: this defines the sequence of frames that cause the problem. Each array element is a tree.

export const data = [
  {
    // 9
    className: "Stack",
    children: [
      {
        className: "Stack",
        children: [
          {
            className: "Stack",
            children: [],
            id: "TreeBase-117",
          },
          {
            className: "Stack",
            children: [],
            id: "TreeBase-118",
          },
          {
            className: "Stack",
            children: [
              {
                className: "Stack",
                children: [],
                id: "TreeBase-122",
              },
            ],
            id: "TreeBase-121",
          },
        ],
        id: "TreeBase-5",
      },
    ],
    id: "TreeBase-4",
  },
  {
    // 8
    className: "Stack",
    children: [
      {
        className: "Stack",
        children: [
          {
            className: "Stack",
            children: [],
            id: "TreeBase-117",
          },
          {
            className: "Stack",
            children: [],
            id: "TreeBase-118",
          },
          {
            className: "Stack",
            children: [],
            id: "TreeBase-121",
          },
        ],
        id: "TreeBase-5",
      },
    ],
    id: "TreeBase-4",
  },
  {
    // 7
    className: "Stack",
    children: [
      {
        className: "Stack",
        children: [
          {
            className: "Stack",
            children: [
              {
                className: "Gap",
                children: [],
                id: "TreeBase-120",
              },
            ],
            id: "TreeBase-117",
          },
          {
            className: "Stack",
            children: [],
            id: "TreeBase-118",
          },
          {
            className: "Stack",
            children: [],
            id: "TreeBase-121",
          },
        ],
        id: "TreeBase-5",
      },
    ],
    id: "TreeBase-4",
  },
  {
    // 6
    className: "Stack",
    children: [
      {
        className: "Stack",
        children: [
          {
            className: "Stack",
            children: [
              {
                className: "Gap",
                children: [],
                id: "TreeBase-120",
              },
            ],
            id: "TreeBase-117",
          },
          {
            className: "Stack",
            children: [],
            id: "TreeBase-118",
          },
          {
            className: "Gap",
            children: [],
            id: "TreeBase-119",
          },
          {
            className: "Stack",
            children: [],
            id: "TreeBase-121",
          },
        ],
        id: "TreeBase-5",
      },
    ],
    id: "TreeBase-4",
  },
];

About this issue

  • Original URL
  • State: closed
  • Created 6 months ago
  • Comments: 15 (8 by maintainers)

Commits related to this issue

Most upvoted comments

To whom it might concern, the MR to update the well known benchmark is here: https://github.com/krausest/js-framework-benchmark/pull/1576

Right now, I can see with keyed results that latest is scoring 1.00 VS 1.02 while non-keyed is mostly the same, but on memory consumption it wins by 0.01 margin in both cases, last time I’ve checked.

I’ll try to answer this evening as I might have a “good solution fit them all” thing but I need to test it … until I am sure how I want to resolve these issues there’s not much point in defining anything as even those cases might be allowed, as it used to be in V3. A bit of patience, right now I think you perfectly got how the current revision works though 👍