Re: [w3c/webcomponents] Lazy Custom Element Definitions (#782)

The version that @sorvell and I have been working on requires that the definition is returned synchronously - so, somewhat different - but I think it's helped expose some corner cases that would also be relevant to an async version. The tricky bits I ran into seem to be mostly around situations other than "the browser came across some name and wants to call its constructor" that arguably need to trigger the lazy definition getter. (But I think we didn't run into more complicated timing issues only because we decided not to support async initially.) Particularly, CustomElementRegistry's `define`, `get`, and possibly `whenDefined` imply running the getter in some cases.

For example, what does calling `get` mean if the definition is lazy and hasn't been fetched yet? Currently, `get` can synchronously return either a constructor or `undefined` but you can't sync return the constructor for a lazy definition yet `undefined` doesn't seem right either. One option would be to say that, even if an async lazy definition exists, it should do nothing and return `undefined`. Or, `get` could run the getter, but then deciding on the return value is more murky: would it be OK to change `get` to return a promise if the definition requested was lazy? Or, should `get` return `undefined` but still trigger the getter? Or, maybe we need to add a new way to interact with possibly-lazy definitions?

Another few corner cases stem from a weird behavior of `define`: if any user code that is run sync during [element definition](https://html.spec.whatwg.org/C/#element-definition) (e.g. while getting `observedAttributes` or any of the callbacks) throws, the algorithm rethrows the error without setting anything that would tombstone that name or resolve any promises returned by `whenDefined` - you explicitly haven't burned the name if your definition fails. For example, you can do this:

```javascript
try {
  customElements.define('custom-element', class extends HTMLElement {
    get connectedCallback() { throw new Error('Oh no!'); }
  });
} catch (e) {
  console.error('Caught an error:', e);
}

customElements.define('custom-element', class extends HTMLElement {
  constructor() {
    super();
    console.log('upgraded');
  }
});

document.createElement('custom-element'); // Logs "upgraded".
```

If a lazy definition for a particular name already exists and a user tries to call `define` with the same name, arguably you should run the getter and try to force the definition into existence so that you can see if the lazy definition would or would not prevent the new definition from going through. In our sync version, we run the getter immediately during the new `define` and can keep the same contract for `get` because we can know synchronously whether or not the existing lazy definition would block the new one. However, this seems like a problem for async lazy because you wouldn't be able to know whether or not the new definition should be blocked. Maybe old async lazy definitions that haven't been fetched should be dropped in favor of any new definition?

There's also the question of what `whenDefined` promises for a particular name do when a lazy definition is added for that name as well as the opposite (lazy definition added first, then `whenDefined` called for that name). One way to use `whenDefined` is to wait until a definition exists before creating / using elements having that name. Should calling `whenDefined` for a name with an unfetched lazy definition be considered enough to fetch it?

---

On the feature in general, I feel like it's going to be difficult to use if you have to watch for individual events from every element some tree to know that the tree is completely ready and you can show it to the user without it being broken. Maybe there needs to be some function to get a promise that resolves when all the descendants of a particular node have had their definitions fetched and run:

```javascript
const someTree = template.content.cloneNode(true);
customElements.upgrade(someTree);
await customElements.waitForAllTheLazyDefinitionsPlease(someTree);
otherPlace.appendChild(someTree);
```

-- 
You are receiving this because you are subscribed to this thread.
Reply to this email directly or view it on GitHub:
https://github.com/w3c/webcomponents/issues/782#issuecomment-508308991

Received on Thursday, 4 July 2019 01:53:05 UTC