Custom elements reactions should have a queue per element
authorrniwa@webkit.org <rniwa@webkit.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Tue, 25 Oct 2016 06:18:13 +0000 (06:18 +0000)
committerrniwa@webkit.org <rniwa@webkit.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Tue, 25 Oct 2016 06:18:13 +0000 (06:18 +0000)
https://bugs.webkit.org/show_bug.cgi?id=163878

Reviewed by Antti Koivisto.

Source/WebCore:

This patch splits the custom elements reaction queue into per element to match the latest HTML specifications:
https://html.spec.whatwg.org/multipage/scripting.html#custom-element-reaction-queue
and introduces the backup element queue:
https://html.spec.whatwg.org/multipage/scripting.html#backup-element-queue

In terms of code changes, CustomElementReactionStack now holds onto ElementQueue, an ordered list of elements,
and make each ElementRareData keep its own CustomElementReactionQueue. CustomElementReactionQueue is created
for each custom element when it is synchronously constructed or enqueued to upgrade.

Because each reaction queue is now specific to each element, CustomElementReactionQueue instead of
CustomElementReactionQueueItem stores JSCustomElementInterface.

The backup element queue is created as a singleton returned by CustomElementReactionStack's backupElementQueue,
and ensureBackupQueue() schedules a new mirotask to process the backup queue when there isn't already one.

ensureCurrentQueue() now returns a reference to CustomElementReactionQueue instead of a pointer since it can
fallback to the backup queue when the stack is empty as specified:
https://html.spec.whatwg.org/multipage/scripting.html#enqueue-an-element-on-the-appropriate-element-queue

Note that ensureCurrentQueue() may insert the same element multiple times into the element queue for now since
avoiding this duplication would require either doing O(n) iteration on m_elements or adding a HashSet.
We can revisit this in the future if the reaction queue is found to grow beyond a few entries since elements in
the element queue will have duplicates only when each reaction queue has more than one item.

Tests: fast/custom-elements/backup-element-queue.html
       fast/custom-elements/custom-element-reaction-queue.html

* bindings/js/JSCustomElementInterface.cpp:
(WebCore::JSCustomElementInterface::upgradeElement):
* dom/CustomElementReactionQueue.cpp:
(WebCore::CustomElementReactionQueueItem::CustomElementReactionQueueItem):
(WebCore::CustomElementReactionQueueItem::invoke): Removed the check for isFailedCustomElement since the queue
is explicitly cleared in Element::setIsFailedCustomElement.
(WebCore::CustomElementReactionQueue::CustomElementReactionQueue): Now takes JSCustomElementInterface since
each item in the queue no longer stores Element or JSCustomElementInterface.
(WebCore::CustomElementReactionQueue::clear):
(WebCore::CustomElementReactionQueue::enqueueElementUpgrade):
(WebCore::CustomElementReactionQueue::enqueueElementUpgradeIfDefined):
(WebCore::CustomElementReactionQueue::enqueueConnectedCallbackIfNeeded):
(WebCore::CustomElementReactionQueue::enqueueDisconnectedCallbackIfNeeded):
(WebCore::CustomElementReactionQueue::enqueueAdoptedCallbackIfNeeded):
(WebCore::CustomElementReactionQueue::enqueueAttributeChangedCallbackIfNeeded):
(WebCore::CustomElementReactionQueue::enqueuePostUpgradeReactions):
(WebCore::CustomElementReactionQueue::invokeAll):
(WebCore::CustomElementReactionStack::ElementQueue::add): Added.
(WebCore::CustomElementReactionStack::ElementQueue::invokeAll): Added.
(WebCore::CustomElementReactionStack::ensureCurrentQueue):
(WebCore::BackupElementQueueMicrotask): Added.
(WebCore::CustomElementReactionStack::ensureBackupQueue): Added.
(WebCore::CustomElementReactionStack::processBackupQueue): Added.
(WebCore::CustomElementReactionStack::backupElementQueue): Added.
* dom/CustomElementReactionQueue.h:
* dom/CustomElementRegistry.cpp:
(WebCore::enqueueUpgradeInShadowIncludingTreeOrder):
* dom/Document.cpp:
(WebCore::createFallbackHTMLElement):
* dom/Element.cpp:
(WebCore::Element::setIsDefinedCustomElement): Create a new reaction queue if there isn't already one; when
this element had been upgraded, the reaction queue have already been created in Element::enqueueToUpgrade.
(WebCore::Element::setIsFailedCustomElement): Clear the reaction queue when the upgrading had failed.
(WebCore::Element::enqueueToUpgrade): Added.
(WebCore::Element::reactionQueue): Added.
* dom/Element.h:
* dom/ElementRareData.h:
(WebCore::ElementRareData::customElementReactionQueue): Replaced customElementInterface.
(WebCore::ElementRareData::setCustomElementReactionQueue): Replaced setCustomElementReactionQueue.

LayoutTests:

Added a W3C style testharness.js test for making sure the custom element reaction queue exists per element,
and added a WebKit style test for making sure that the backup element queue exists.

* fast/custom-elements/backup-element-queue-expected.txt: Added.
* fast/custom-elements/backup-element-queue.html: Added.
* fast/custom-elements/custom-element-reaction-queue-expected.txt: Added.
* fast/custom-elements/custom-element-reaction-queue.html: Added.

git-svn-id: https://svn.webkit.org/repository/webkit/trunk@207810 268f45cc-cd09-0410-ab3c-d52691b4dbfc

14 files changed:
LayoutTests/ChangeLog
LayoutTests/fast/custom-elements/backup-element-queue-expected.txt [new file with mode: 0644]
LayoutTests/fast/custom-elements/backup-element-queue.html [new file with mode: 0644]
LayoutTests/fast/custom-elements/custom-element-reaction-queue-expected.txt [new file with mode: 0644]
LayoutTests/fast/custom-elements/custom-element-reaction-queue.html [new file with mode: 0644]
Source/WebCore/ChangeLog
Source/WebCore/bindings/js/JSCustomElementInterface.cpp
Source/WebCore/dom/CustomElementReactionQueue.cpp
Source/WebCore/dom/CustomElementReactionQueue.h
Source/WebCore/dom/CustomElementRegistry.cpp
Source/WebCore/dom/Document.cpp
Source/WebCore/dom/Element.cpp
Source/WebCore/dom/Element.h
Source/WebCore/dom/ElementRareData.h

index 09e4fc9..2d28dd9 100644 (file)
@@ -1,3 +1,18 @@
+2016-10-24  Ryosuke Niwa  <rniwa@webkit.org>
+
+        Custom elements reactions should have a queue per element
+        https://bugs.webkit.org/show_bug.cgi?id=163878
+
+        Reviewed by Antti Koivisto.
+
+        Added a W3C style testharness.js test for making sure the custom element reaction queue exists per element,
+        and added a WebKit style test for making sure that the backup element queue exists.
+
+        * fast/custom-elements/backup-element-queue-expected.txt: Added.
+        * fast/custom-elements/backup-element-queue.html: Added.
+        * fast/custom-elements/custom-element-reaction-queue-expected.txt: Added.
+        * fast/custom-elements/custom-element-reaction-queue.html: Added.
+
 2016-10-24  Jiewen Tan  <jiewen_tan@apple.com>
 
         Update SubtleCrypto::generateKey to match the latest spec
diff --git a/LayoutTests/fast/custom-elements/backup-element-queue-expected.txt b/LayoutTests/fast/custom-elements/backup-element-queue-expected.txt
new file mode 100644 (file)
index 0000000..e3f7059
--- /dev/null
@@ -0,0 +1,15 @@
+This tests the existence of the backup element queue. To manually test, press the delete key once the page is loaded.
+
+On success, you will see a series of "PASS" messages, followed by "TEST COMPLETE".
+
+
+PASS constructed is false
+PASS editor.innerHTML = "a<test-element>b</test-element>c"; constructed is true
+PASS editor.focus(); getSelection().selectAllChildren(editor); disconnected is false
+PASS "Before the end of the micro task"; disconnected is false
+PASS "At the end of the micro task"; disconnected is true
+PASS successfullyParsed is true
+
+TEST COMPLETE
+
+
diff --git a/LayoutTests/fast/custom-elements/backup-element-queue.html b/LayoutTests/fast/custom-elements/backup-element-queue.html
new file mode 100644 (file)
index 0000000..68b7367
--- /dev/null
@@ -0,0 +1,51 @@
+<!DOCTYPE html>
+<html>
+<body>
+<script src="../../resources/js-test.js"></script>
+<div id="editor" contenteditable></div>
+<script>
+
+description('This tests the existence of the backup element queue. To manually test, press the delete key once the page is loaded.');
+
+var jsTestIsAsync = true;
+
+var constructed = false;
+var disconnected = false;
+class TestElement extends HTMLElement {
+    constructor() {
+        super();
+        constructed = true;
+    }
+    disconnectedCallback() {
+        disconnected = true;
+    }
+}
+customElements.define('test-element', TestElement);
+
+var editor = document.getElementById('editor');
+shouldBe('constructed', 'false');
+shouldBe('editor.innerHTML = "a<test-element>b</test-element>c"; constructed', 'true');
+shouldBe('editor.focus(); getSelection().selectAllChildren(editor); disconnected', 'false');
+
+function checkDisconnectedAtEndOfMictotask()
+{
+    shouldBe('"Before the end of the micro task"; disconnected', 'false');
+    Promise.resolve().then(function () {
+        shouldBe('"At the end of the micro task"; disconnected', 'true');
+        finishJSTest();
+    });
+}
+
+if (window.testRunner) {
+    testRunner.execCommand('delete', false, null);
+    checkDisconnectedAtEndOfMictotask();
+} else {
+    editor.oninput = function () {
+        checkDisconnectedAtEndOfMictotask();
+    }
+}
+
+
+</script>
+</body>
+</html>
diff --git a/LayoutTests/fast/custom-elements/custom-element-reaction-queue-expected.txt b/LayoutTests/fast/custom-elements/custom-element-reaction-queue-expected.txt
new file mode 100644 (file)
index 0000000..5b50d4f
--- /dev/null
@@ -0,0 +1,5 @@
+
+PASS Upgrading a custom element must invoke attributeChangedCallback and connectedCallback before start upgrading another element 
+PASS Mutating a undefined custom element while upgrading a custom element must not enqueue or invoke reactions on the mutated element 
+PASS Mutating another custom element inside adopted callback must invoke all pending callbacks on the mutated element 
+
diff --git a/LayoutTests/fast/custom-elements/custom-element-reaction-queue.html b/LayoutTests/fast/custom-elements/custom-element-reaction-queue.html
new file mode 100644 (file)
index 0000000..1546440
--- /dev/null
@@ -0,0 +1,181 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>Custom Elements: Each element must have its own custom element reaction queue</title>
+<meta name="author" title="Ryosuke Niwa" href="mailto:rniwa@webkit.org">
+<meta name="assert" content="Each element must have its own custom element reaction queue">
+<meta name="help" content="https://html.spec.whatwg.org/multipage/scripting.html#custom-element-reaction-queue">
+<script src="../../resources/testharness.js"></script>
+<script src="../../resources/testharnessreport.js"></script>
+<script src="../../imported/w3c/web-platform-tests/custom-elements/resources/custom-elements-helpers.js"></script>
+</head>
+<body>
+<div id="log"></div>
+<script>
+
+function create_constructor_log(element) {
+    return {type: 'constructed', element: element};
+}
+
+function assert_constructor_log_entry(log, element) {
+    assert_equals(log.type, 'constructed');
+    assert_equals(log.element, element);
+}
+
+function assert_adopted_log_entry(log, element) {
+    assert_equals(log.type, 'adopted');
+    assert_equals(log.element, element);
+}
+
+function create_adopted_callback_log(element) {
+    return {type: 'adopted', element: element};
+}
+
+function create_connected_callback_log(element) {
+    return {type: 'connected', element: element};
+}
+
+function assert_connected_log_entry(log, element) {
+    assert_equals(log.type, 'connected');
+    assert_equals(log.element, element);
+}
+
+test_with_window(function (contentWindow) {
+    const contentDocument = contentWindow.document;
+    contentDocument.write('<test-element id="first-element">');
+    contentDocument.write('<test-element id="second-element">');
+
+    const element1 = contentDocument.getElementById('first-element');
+    const element2 = contentDocument.getElementById('second-element');
+    assert_equals(Object.getPrototypeOf(element1), contentWindow.HTMLElement.prototype);
+    assert_equals(Object.getPrototypeOf(element2), contentWindow.HTMLElement.prototype);
+
+    let log = [];
+    class TestElement extends contentWindow.HTMLElement {
+        constructor() {
+            super();
+            log.push(create_constructor_log(this));
+        }
+        connectedCallback(...args) {
+            log.push(create_connected_callback_log(this, ...args));
+        }
+        attributeChangedCallback(...args) {
+            log.push(create_attribute_changed_callback_log(this, ...args));
+        }
+        static get observedAttributes() { return ['id']; }
+    }
+    contentWindow.customElements.define('test-element', TestElement);
+    assert_equals(Object.getPrototypeOf(element1), TestElement.prototype);
+    assert_equals(Object.getPrototypeOf(element2), TestElement.prototype);
+
+    assert_equals(log.length, 6);
+    assert_constructor_log_entry(log[0], element1);
+    assert_attribute_log_entry(log[1], {name: 'id', oldValue: null, newValue: 'first-element', namespace: null});
+    assert_connected_log_entry(log[2], element1);
+    assert_constructor_log_entry(log[3], element2);
+    assert_attribute_log_entry(log[4], {name: 'id', oldValue: null, newValue: 'second-element', namespace: null});
+    assert_connected_log_entry(log[5], element2);
+}, 'Upgrading a custom element must invoke attributeChangedCallback and connectedCallback before start upgrading another element');
+
+test_with_window(function (contentWindow) {
+    const contentDocument = contentWindow.document;
+    contentDocument.write('<test-element id="first-element">');
+    contentDocument.write('<test-element id="second-element">');
+
+    const element1 = contentDocument.getElementById('first-element');
+    const element2 = contentDocument.getElementById('second-element');
+    assert_equals(Object.getPrototypeOf(element1), contentWindow.HTMLElement.prototype);
+    assert_equals(Object.getPrototypeOf(element2), contentWindow.HTMLElement.prototype);
+
+    let log = [];
+    class TestElement extends contentWindow.HTMLElement {
+        constructor() {
+            super();
+            log.push(create_constructor_log(this));
+            if (this == element1)
+                element2.setAttribute('class', 'foo');
+        }
+        connectedCallback(...args) {
+            log.push(create_connected_callback_log(this, ...args));
+        }
+        attributeChangedCallback(...args) {
+            log.push(create_attribute_changed_callback_log(this, ...args));
+        }
+        static get observedAttributes() { return ['id', 'class']; }
+    }
+    contentWindow.customElements.define('test-element', TestElement);
+    assert_equals(Object.getPrototypeOf(element1), TestElement.prototype);
+    assert_equals(Object.getPrototypeOf(element2), TestElement.prototype);
+
+    assert_equals(log.length, 7);
+    assert_constructor_log_entry(log[0], element1);
+    assert_attribute_log_entry(log[1], {name: 'id', oldValue: null, newValue: 'first-element', namespace: null});
+    assert_connected_log_entry(log[2], element1);
+    assert_constructor_log_entry(log[3], element2);
+    assert_attribute_log_entry(log[4], {name: 'id', oldValue: null, newValue: 'second-element', namespace: null});
+    assert_attribute_log_entry(log[5], {name: 'class', oldValue: null, newValue: 'foo', namespace: null});
+    assert_connected_log_entry(log[6], element2);
+}, 'Mutating a undefined custom element while upgrading a custom element must not enqueue or invoke reactions on the mutated element');
+
+test_with_window(function (contentWindow) {
+    let log = [];
+    let element1;
+    let element2;
+    class TestElement extends contentWindow.HTMLElement {
+        constructor() {
+            super();
+            log.push(create_constructor_log(this));
+        }
+        adoptedCallback(...args) {
+            log.push(create_adopted_callback_log(this, ...args));
+            if (this == element1)
+                element3.setAttribute('id', 'foo');
+        }
+        connectedCallback(...args) {
+            log.push(create_connected_callback_log(this, ...args));
+        }
+        attributeChangedCallback(...args) {
+            log.push(create_attribute_changed_callback_log(this, ...args));
+        }
+        static get observedAttributes() { return ['id', 'class']; }
+    }
+
+    contentWindow.customElements.define('test-element', TestElement);
+
+    let contentDocument = contentWindow.document;
+    element1 = contentDocument.createElement('test-element');
+    element2 = contentDocument.createElement('test-element');
+    element3 = contentDocument.createElement('test-element');
+    assert_equals(Object.getPrototypeOf(element1), TestElement.prototype);
+    assert_equals(Object.getPrototypeOf(element2), TestElement.prototype);
+    assert_equals(Object.getPrototypeOf(element3), TestElement.prototype);
+
+    assert_equals(log.length, 3);
+    assert_constructor_log_entry(log[0], element1);
+    assert_constructor_log_entry(log[1], element2);
+    assert_constructor_log_entry(log[2], element3);
+    log = [];
+
+    const container = contentDocument.createElement('div');
+    container.appendChild(element1);
+    container.appendChild(element2);
+    container.appendChild(element3);
+
+    const anotherDocument = document.implementation.createHTMLDocument();
+    anotherDocument.documentElement.appendChild(container);
+
+    assert_equals(log.length, 7);
+    assert_adopted_log_entry(log[0], element1);
+    assert_adopted_log_entry(log[1], element3);
+    assert_connected_log_entry(log[2], element3);
+    assert_attribute_log_entry(log[3], {name: 'id', oldValue: null, newValue: 'foo', namespace: null});
+    assert_connected_log_entry(log[4], element1);
+    assert_adopted_log_entry(log[5], element2);
+    assert_connected_log_entry(log[6], element2);
+
+}, 'Mutating another custom element inside adopted callback must invoke all pending callbacks on the mutated element');
+
+
+</script>
+</body>
+</html>
index a358c81..80fb00d 100644 (file)
@@ -1,3 +1,77 @@
+2016-10-24  Ryosuke Niwa  <rniwa@webkit.org>
+
+        Custom elements reactions should have a queue per element
+        https://bugs.webkit.org/show_bug.cgi?id=163878
+
+        Reviewed by Antti Koivisto.
+
+        This patch splits the custom elements reaction queue into per element to match the latest HTML specifications:
+        https://html.spec.whatwg.org/multipage/scripting.html#custom-element-reaction-queue
+        and introduces the backup element queue:
+        https://html.spec.whatwg.org/multipage/scripting.html#backup-element-queue
+
+        In terms of code changes, CustomElementReactionStack now holds onto ElementQueue, an ordered list of elements,
+        and make each ElementRareData keep its own CustomElementReactionQueue. CustomElementReactionQueue is created
+        for each custom element when it is synchronously constructed or enqueued to upgrade.
+
+        Because each reaction queue is now specific to each element, CustomElementReactionQueue instead of
+        CustomElementReactionQueueItem stores JSCustomElementInterface.
+
+        The backup element queue is created as a singleton returned by CustomElementReactionStack's backupElementQueue,
+        and ensureBackupQueue() schedules a new mirotask to process the backup queue when there isn't already one.
+
+        ensureCurrentQueue() now returns a reference to CustomElementReactionQueue instead of a pointer since it can
+        fallback to the backup queue when the stack is empty as specified:
+        https://html.spec.whatwg.org/multipage/scripting.html#enqueue-an-element-on-the-appropriate-element-queue
+
+        Note that ensureCurrentQueue() may insert the same element multiple times into the element queue for now since
+        avoiding this duplication would require either doing O(n) iteration on m_elements or adding a HashSet.
+        We can revisit this in the future if the reaction queue is found to grow beyond a few entries since elements in
+        the element queue will have duplicates only when each reaction queue has more than one item.
+
+        Tests: fast/custom-elements/backup-element-queue.html
+               fast/custom-elements/custom-element-reaction-queue.html
+
+        * bindings/js/JSCustomElementInterface.cpp:
+        (WebCore::JSCustomElementInterface::upgradeElement):
+        * dom/CustomElementReactionQueue.cpp:
+        (WebCore::CustomElementReactionQueueItem::CustomElementReactionQueueItem):
+        (WebCore::CustomElementReactionQueueItem::invoke): Removed the check for isFailedCustomElement since the queue
+        is explicitly cleared in Element::setIsFailedCustomElement.
+        (WebCore::CustomElementReactionQueue::CustomElementReactionQueue): Now takes JSCustomElementInterface since
+        each item in the queue no longer stores Element or JSCustomElementInterface.
+        (WebCore::CustomElementReactionQueue::clear):
+        (WebCore::CustomElementReactionQueue::enqueueElementUpgrade):
+        (WebCore::CustomElementReactionQueue::enqueueElementUpgradeIfDefined):
+        (WebCore::CustomElementReactionQueue::enqueueConnectedCallbackIfNeeded):
+        (WebCore::CustomElementReactionQueue::enqueueDisconnectedCallbackIfNeeded):
+        (WebCore::CustomElementReactionQueue::enqueueAdoptedCallbackIfNeeded):
+        (WebCore::CustomElementReactionQueue::enqueueAttributeChangedCallbackIfNeeded):
+        (WebCore::CustomElementReactionQueue::enqueuePostUpgradeReactions):
+        (WebCore::CustomElementReactionQueue::invokeAll):
+        (WebCore::CustomElementReactionStack::ElementQueue::add): Added.
+        (WebCore::CustomElementReactionStack::ElementQueue::invokeAll): Added.
+        (WebCore::CustomElementReactionStack::ensureCurrentQueue):
+        (WebCore::BackupElementQueueMicrotask): Added.
+        (WebCore::CustomElementReactionStack::ensureBackupQueue): Added.
+        (WebCore::CustomElementReactionStack::processBackupQueue): Added.
+        (WebCore::CustomElementReactionStack::backupElementQueue): Added.
+        * dom/CustomElementReactionQueue.h:
+        * dom/CustomElementRegistry.cpp:
+        (WebCore::enqueueUpgradeInShadowIncludingTreeOrder):
+        * dom/Document.cpp:
+        (WebCore::createFallbackHTMLElement):
+        * dom/Element.cpp:
+        (WebCore::Element::setIsDefinedCustomElement): Create a new reaction queue if there isn't already one; when
+        this element had been upgraded, the reaction queue have already been created in Element::enqueueToUpgrade.
+        (WebCore::Element::setIsFailedCustomElement): Clear the reaction queue when the upgrading had failed.
+        (WebCore::Element::enqueueToUpgrade): Added.
+        (WebCore::Element::reactionQueue): Added.
+        * dom/Element.h:
+        * dom/ElementRareData.h:
+        (WebCore::ElementRareData::customElementReactionQueue): Replaced customElementInterface.
+        (WebCore::ElementRareData::setCustomElementReactionQueue): Replaced setCustomElementReactionQueue.
+
 2016-10-24  Jiewen Tan  <jiewen_tan@apple.com>
 
         Update SubtleCrypto::generateKey to match the latest spec
index ac452d9..ff3f301 100644 (file)
@@ -182,7 +182,7 @@ void JSCustomElementInterface::upgradeElement(Element& element)
         return;
     }
 
-    CustomElementReactionQueue::enqueuePostUpgradeReactions(element, *this);
+    CustomElementReactionQueue::enqueuePostUpgradeReactions(element);
 
     m_constructionStack.append(&element);
 
index 67df9ef..ae898ff 100644 (file)
@@ -34,6 +34,7 @@
 #include "Element.h"
 #include "JSCustomElementInterface.h"
 #include "JSDOMBinding.h"
+#include "Microtasks.h"
 #include <heap/Heap.h>
 #include <wtf/Optional.h>
 #include <wtf/Ref.h>
@@ -50,57 +51,47 @@ public:
         AttributeChanged,
     };
 
-    CustomElementReactionQueueItem(Type type, Element& element, JSCustomElementInterface& elementInterface)
+    CustomElementReactionQueueItem(Type type)
         : m_type(type)
-        , m_element(element)
-        , m_interface(elementInterface)
     { }
 
-    CustomElementReactionQueueItem(Element& element, JSCustomElementInterface& elementInterface, Document& oldDocument, Document& newDocument)
+    CustomElementReactionQueueItem(Document& oldDocument, Document& newDocument)
         : m_type(Type::Adopted)
-        , m_element(element)
-        , m_interface(elementInterface)
         , m_oldDocument(&oldDocument)
         , m_newDocument(&newDocument)
     { }
 
-    CustomElementReactionQueueItem(Element& element, JSCustomElementInterface& elementInterface, const QualifiedName& attributeName, const AtomicString& oldValue, const AtomicString& newValue)
+    CustomElementReactionQueueItem(const QualifiedName& attributeName, const AtomicString& oldValue, const AtomicString& newValue)
         : m_type(Type::AttributeChanged)
-        , m_element(element)
-        , m_interface(elementInterface)
         , m_attributeName(attributeName)
         , m_oldValue(oldValue)
         , m_newValue(newValue)
     { }
 
-    void invoke()
+    void invoke(Element& element, JSCustomElementInterface& elementInterface)
     {
-        if (m_element->isFailedCustomElement())
-            return;
         switch (m_type) {
         case Type::ElementUpgrade:
-            m_interface->upgradeElement(m_element.get());
+            elementInterface.upgradeElement(element);
             break;
         case Type::Connected:
-            m_interface->invokeConnectedCallback(m_element.get());
+            elementInterface.invokeConnectedCallback(element);
             break;
         case Type::Disconnected:
-            m_interface->invokeDisconnectedCallback(m_element.get());
+            elementInterface.invokeDisconnectedCallback(element);
             break;
         case Type::Adopted:
-            m_interface->invokeAdoptedCallback(m_element.get(), *m_oldDocument, *m_newDocument);
+            elementInterface.invokeAdoptedCallback(element, *m_oldDocument, *m_newDocument);
             break;
         case Type::AttributeChanged:
             ASSERT(m_attributeName);
-            m_interface->invokeAttributeChangedCallback(m_element.get(), m_attributeName.value(), m_oldValue, m_newValue);
+            elementInterface.invokeAttributeChangedCallback(element, m_attributeName.value(), m_oldValue, m_newValue);
             break;
         }
     }
 
 private:
     Type m_type;
-    Ref<Element> m_element;
-    Ref<JSCustomElementInterface> m_interface;
     RefPtr<Document> m_oldDocument;
     RefPtr<Document> m_newDocument;
     Optional<QualifiedName> m_attributeName;
@@ -108,7 +99,8 @@ private:
     AtomicString m_newValue;
 };
 
-CustomElementReactionQueue::CustomElementReactionQueue()
+CustomElementReactionQueue::CustomElementReactionQueue(JSCustomElementInterface& elementInterface)
+    : m_interface(elementInterface)
 { }
 
 CustomElementReactionQueue::~CustomElementReactionQueue()
@@ -116,11 +108,15 @@ CustomElementReactionQueue::~CustomElementReactionQueue()
     ASSERT(m_items.isEmpty());
 }
 
-void CustomElementReactionQueue::enqueueElementUpgrade(Element& element, JSCustomElementInterface& elementInterface)
+void CustomElementReactionQueue::clear()
 {
-    ASSERT(element.tagQName() == elementInterface.name());
-    if (auto* queue = CustomElementReactionStack::ensureCurrentQueue())
-        queue->m_items.append({CustomElementReactionQueueItem::Type::ElementUpgrade, element, elementInterface});
+    m_items.clear();
+}
+
+void CustomElementReactionQueue::enqueueElementUpgrade(Element& element)
+{
+    auto& queue = CustomElementReactionStack::ensureCurrentQueue(element);
+    queue.m_items.append({CustomElementReactionQueueItem::Type::ElementUpgrade});
 }
 
 void CustomElementReactionQueue::enqueueElementUpgradeIfDefined(Element& element)
@@ -139,97 +135,102 @@ void CustomElementReactionQueue::enqueueElementUpgradeIfDefined(Element& element
     if (!elementInterface)
         return;
 
-    enqueueElementUpgrade(element, *elementInterface);
+    element.enqueueToUpgrade(*elementInterface);
 }
 
 void CustomElementReactionQueue::enqueueConnectedCallbackIfNeeded(Element& element)
 {
     ASSERT(element.isDefinedCustomElement());
-    auto* elementInterface = element.customElementInterface();
-    ASSERT(elementInterface);
-    if (!elementInterface->hasConnectedCallback())
-        return;
-
-    if (auto* queue = CustomElementReactionStack::ensureCurrentQueue())
-        queue->m_items.append({CustomElementReactionQueueItem::Type::Connected, element, *elementInterface});
+    auto& queue = CustomElementReactionStack::ensureCurrentQueue(element);
+    if (queue.m_interface->hasConnectedCallback())
+        queue.m_items.append({CustomElementReactionQueueItem::Type::Connected});
 }
 
 void CustomElementReactionQueue::enqueueDisconnectedCallbackIfNeeded(Element& element)
 {
     ASSERT(element.isDefinedCustomElement());
-    auto* elementInterface = element.customElementInterface();
-    ASSERT(elementInterface);
-    if (!elementInterface->hasDisconnectedCallback())
-        return;
-
-    if (auto* queue = CustomElementReactionStack::ensureCurrentQueue())
-        queue->m_items.append({CustomElementReactionQueueItem::Type::Disconnected, element, *elementInterface});
+    auto& queue = CustomElementReactionStack::ensureCurrentQueue(element);
+    if (queue.m_interface->hasDisconnectedCallback())
+        queue.m_items.append({CustomElementReactionQueueItem::Type::Disconnected});
 }
 
 void CustomElementReactionQueue::enqueueAdoptedCallbackIfNeeded(Element& element, Document& oldDocument, Document& newDocument)
 {
     ASSERT(element.isDefinedCustomElement());
-    auto* elementInterface = element.customElementInterface();
-    ASSERT(elementInterface);
-    if (!elementInterface->hasAdoptedCallback())
-        return;
-
-    if (auto* queue = CustomElementReactionStack::ensureCurrentQueue())
-        queue->m_items.append({element, *elementInterface, oldDocument, newDocument});
+    auto& queue = CustomElementReactionStack::ensureCurrentQueue(element);
+    if (queue.m_interface->hasAdoptedCallback())
+        queue.m_items.append({oldDocument, newDocument});
 }
 
 void CustomElementReactionQueue::enqueueAttributeChangedCallbackIfNeeded(Element& element, const QualifiedName& attributeName, const AtomicString& oldValue, const AtomicString& newValue)
 {
     ASSERT(element.isDefinedCustomElement());
-    auto* elementInterface = element.customElementInterface();
-    ASSERT(elementInterface);
-    if (!elementInterface->observesAttribute(attributeName.localName()))
-        return;
-
-    if (auto* queue = CustomElementReactionStack::ensureCurrentQueue())
-        queue->m_items.append({element, *elementInterface, attributeName, oldValue, newValue});
+    auto& queue = CustomElementReactionStack::ensureCurrentQueue(element);
+    if (queue.m_interface->observesAttribute(attributeName.localName()))
+        queue.m_items.append({attributeName, oldValue, newValue});
 }
 
-void CustomElementReactionQueue::enqueuePostUpgradeReactions(Element& element, JSCustomElementInterface& elementInterface)
+void CustomElementReactionQueue::enqueuePostUpgradeReactions(Element& element)
 {
+    ASSERT(element.isCustomElementUpgradeCandidate());
     if (!element.hasAttributes() && !element.inDocument())
         return;
 
-    auto* queue = CustomElementReactionStack::ensureCurrentQueue();
-    if (!queue)
-        return;
+    auto* queue = element.reactionQueue();
+    ASSERT(queue);
 
     if (element.hasAttributes()) {
         for (auto& attribute : element.attributesIterator()) {
-            if (elementInterface.observesAttribute(attribute.localName()))
-                queue->m_items.append({element, elementInterface, attribute.name(), nullAtom, attribute.value()});
+            if (queue->m_interface->observesAttribute(attribute.localName()))
+                queue->m_items.append({attribute.name(), nullAtom, attribute.value()});
         }
     }
 
-    if (element.inDocument() && elementInterface.hasConnectedCallback())
-        queue->m_items.append({CustomElementReactionQueueItem::Type::Connected, element, elementInterface});
+    if (element.inDocument() && queue->m_interface->hasConnectedCallback())
+        queue->m_items.append({CustomElementReactionQueueItem::Type::Connected});
 }
 
-void CustomElementReactionQueue::invokeAll()
+void CustomElementReactionQueue::invokeAll(Element& element)
 {
-    // FIXME: This queue needs to be per element.
     while (!m_items.isEmpty()) {
         Vector<CustomElementReactionQueueItem> items = WTFMove(m_items);
         for (auto& item : items)
-            item.invoke();
+            item.invoke(element, m_interface.get());
     }
 }
 
-CustomElementReactionQueue* CustomElementReactionStack::ensureCurrentQueue()
+inline void CustomElementReactionStack::ElementQueue::add(Element& element)
 {
-    // FIXME: This early exit indicates a bug that some DOM API is missing CEReactions
-    if (!s_currentProcessingStack)
-        return nullptr;
+    // FIXME: Avoid inserting the same element multiple times.
+    m_elements.append(element);
+}
+
+inline void CustomElementReactionStack::ElementQueue::invokeAll()
+{
+    Vector<Ref<Element>> elements;
+    elements.swap(m_elements);
+    for (auto& element : elements) {
+        auto* queue = element->reactionQueue();
+        ASSERT(queue);
+        queue->invokeAll(element.get());
+    }
+    ASSERT(m_elements.isEmpty());
+}
+
+CustomElementReactionQueue& CustomElementReactionStack::ensureCurrentQueue(Element& element)
+{
+    ASSERT(element.reactionQueue());
+    if (!s_currentProcessingStack) {
+        auto& queue = CustomElementReactionStack::ensureBackupQueue();
+        queue.add(element);
+        return *element.reactionQueue();
+    }
 
     auto*& queue = s_currentProcessingStack->m_queue;
     if (!queue) // We use a raw pointer to avoid genearing code to delete it in ~CustomElementReactionStack.
-        queue = new CustomElementReactionQueue;
-    return queue;
+        queue = new ElementQueue;
+    queue->add(element);
+    return *element.reactionQueue();
 }
 
 CustomElementReactionStack* CustomElementReactionStack::s_currentProcessingStack = nullptr;
@@ -242,6 +243,39 @@ void CustomElementReactionStack::processQueue()
     m_queue = nullptr;
 }
 
+class BackupElementQueueMicrotask final : public Microtask {
+    WTF_MAKE_FAST_ALLOCATED;
+private:
+    Result run() final
+    {
+        CustomElementReactionStack::processBackupQueue();
+        return Result::Done;
+    }
+};
+
+static bool s_processingBackupElementQueue = false;
+
+CustomElementReactionStack::ElementQueue& CustomElementReactionStack::ensureBackupQueue()
+{
+    if (!s_processingBackupElementQueue) {
+        s_processingBackupElementQueue = true;
+        MicrotaskQueue::mainThreadQueue().append(std::make_unique<BackupElementQueueMicrotask>());
+    }
+    return backupElementQueue();
+}
+
+void CustomElementReactionStack::processBackupQueue()
+{
+    backupElementQueue().invokeAll();
+    s_processingBackupElementQueue = false;
+}
+
+CustomElementReactionStack::ElementQueue& CustomElementReactionStack::backupElementQueue()
+{
+    static NeverDestroyed<ElementQueue> queue;
+    return queue.get();
+}
+
 }
 
 #endif
index 77f0607..9c879d7 100644 (file)
@@ -42,20 +42,22 @@ class QualifiedName;
 class CustomElementReactionQueue {
     WTF_MAKE_NONCOPYABLE(CustomElementReactionQueue);
 public:
-    CustomElementReactionQueue();
+    CustomElementReactionQueue(JSCustomElementInterface&);
     ~CustomElementReactionQueue();
 
-    static void enqueueElementUpgrade(Element&, JSCustomElementInterface&);
+    static void enqueueElementUpgrade(Element&);
     static void enqueueElementUpgradeIfDefined(Element&);
     static void enqueueConnectedCallbackIfNeeded(Element&);
     static void enqueueDisconnectedCallbackIfNeeded(Element&);
     static void enqueueAdoptedCallbackIfNeeded(Element&, Document& oldDocument, Document& newDocument);
     static void enqueueAttributeChangedCallbackIfNeeded(Element&, const QualifiedName&, const AtomicString& oldValue, const AtomicString& newValue);
-    static void enqueuePostUpgradeReactions(Element&, JSCustomElementInterface&);
+    static void enqueuePostUpgradeReactions(Element&);
 
-    void invokeAll();
+    void invokeAll(Element&);
+    void clear();
 
 private:
+    Ref<JSCustomElementInterface> m_interface;
     Vector<CustomElementReactionQueueItem> m_items;
 };
 
@@ -74,15 +76,28 @@ public:
         s_currentProcessingStack = m_previousProcessingStack;
     }
 
-    // FIXME: This should be a reference once "ensure" starts to work.
-    static CustomElementReactionQueue* ensureCurrentQueue();
+    static CustomElementReactionQueue& ensureCurrentQueue(Element&);
 
     static bool hasCurrentProcessingStack() { return s_currentProcessingStack; }
 
+    static void processBackupQueue();
+
 private:
+    class ElementQueue {
+    public:
+        void add(Element&);
+        void invokeAll();
+
+    private:
+        Vector<Ref<Element>> m_elements;
+    };
+
     WEBCORE_EXPORT void processQueue();
 
-    CustomElementReactionQueue* m_queue { nullptr };
+    static ElementQueue& ensureBackupQueue();
+    static ElementQueue& backupElementQueue();
+
+    ElementQueue* m_queue { nullptr };
     CustomElementReactionStack* m_previousProcessingStack;
 
     WEBCORE_EXPORT static CustomElementReactionStack* s_currentProcessingStack;
index 81f7ca1..f984f91 100644 (file)
@@ -60,7 +60,7 @@ static void enqueueUpgradeInShadowIncludingTreeOrder(ContainerNode& node, JSCust
 {
     for (Element* element = ElementTraversal::firstWithin(node); element; element = ElementTraversal::next(*element)) {
         if (element->isCustomElementUpgradeCandidate() && element->tagQName() == elementInterface.name())
-            CustomElementReactionQueue::enqueueElementUpgrade(*element, elementInterface);
+            element->enqueueToUpgrade(elementInterface);
         if (auto* shadowRoot = element->shadowRoot()) {
             if (shadowRoot->mode() != ShadowRoot::Mode::UserAgent)
                 enqueueUpgradeInShadowIncludingTreeOrder(*shadowRoot, elementInterface);
index 110e647..3408b7b 100644 (file)
@@ -1075,8 +1075,7 @@ static Ref<HTMLElement> createFallbackHTMLElement(Document& document, const Qual
         if (UNLIKELY(registry)) {
             if (auto* elementInterface = registry->findInterface(name)) {
                 auto element = HTMLElement::create(name, document);
-                element->setIsCustomElementUpgradeCandidate();
-                CustomElementReactionQueue::enqueueElementUpgrade(element.get(), *elementInterface);
+                element->enqueueToUpgrade(*elementInterface);
                 return element;
             }
         }
index dbbbd14..873aa3a 100644 (file)
@@ -1902,15 +1902,22 @@ void Element::setIsDefinedCustomElement(JSCustomElementInterface& elementInterfa
 {
     clearFlag(IsEditingTextOrUndefinedCustomElementFlag);
     setFlag(IsCustomElement);
-    ensureElementRareData().setCustomElementInterface(elementInterface);
+    auto& data = ensureElementRareData();
+    if (!data.customElementReactionQueue())
+        data.setCustomElementReactionQueue(std::make_unique<CustomElementReactionQueue>(elementInterface));
 }
 
-void Element::setIsFailedCustomElement(JSCustomElementInterface& elementInterface)
+void Element::setIsFailedCustomElement(JSCustomElementInterface&)
 {
     ASSERT(isUndefinedCustomElement());
     ASSERT(getFlag(IsEditingTextOrUndefinedCustomElementFlag));
     clearFlag(IsCustomElement);
-    ensureElementRareData().setCustomElementInterface(elementInterface);
+
+    if (hasRareData()) {
+        // Clear the queue instead of deleting it since this function can be called inside CustomElementReactionQueue::invokeAll during upgrades.
+        if (auto* queue = elementRareData()->customElementReactionQueue())
+            queue->clear();
+    }
 }
 
 void Element::setIsCustomElementUpgradeCandidate()
@@ -1920,12 +1927,25 @@ void Element::setIsCustomElementUpgradeCandidate()
     setFlag(IsEditingTextOrUndefinedCustomElementFlag);
 }
 
-JSCustomElementInterface* Element::customElementInterface() const
+void Element::enqueueToUpgrade(JSCustomElementInterface& elementInterface)
+{
+    ASSERT(!isDefinedCustomElement() && !isFailedCustomElement());
+    setFlag(IsCustomElement);
+    setFlag(IsEditingTextOrUndefinedCustomElementFlag);
+
+    auto& data = ensureElementRareData();
+    ASSERT(!data.customElementReactionQueue());
+
+    data.setCustomElementReactionQueue(std::make_unique<CustomElementReactionQueue>(elementInterface));
+    data.customElementReactionQueue()->enqueueElementUpgrade(*this);
+}
+
+CustomElementReactionQueue* Element::reactionQueue() const
 {
-    ASSERT(isDefinedCustomElement());
+    ASSERT(isDefinedCustomElement() || isCustomElementUpgradeCandidate());
     if (!hasRareData())
         return nullptr;
-    return elementRareData()->customElementInterface();
+    return elementRareData()->customElementReactionQueue();
 }
 
 #endif
index c6e5899..a1bd301 100644 (file)
@@ -39,6 +39,7 @@ namespace WebCore {
 
 class ClientRect;
 class ClientRectList;
+class CustomElementReactionQueue;
 class DatasetDOMStringMap;
 class Dictionary;
 class DOMTokenList;
@@ -284,7 +285,8 @@ public:
     void setIsDefinedCustomElement(JSCustomElementInterface&);
     void setIsFailedCustomElement(JSCustomElementInterface&);
     void setIsCustomElementUpgradeCandidate();
-    JSCustomElementInterface* customElementInterface() const;
+    void enqueueToUpgrade(JSCustomElementInterface&);
+    CustomElementReactionQueue* reactionQueue() const;
 #endif
 
     // FIXME: this should not be virtual, do not override this.
index 84a2189..25b8efe 100644 (file)
@@ -22,6 +22,7 @@
 #ifndef ElementRareData_h
 #define ElementRareData_h
 
+#include "CustomElementReactionQueue.h"
 #include "DOMTokenList.h"
 #include "DatasetDOMStringMap.h"
 #include "JSCustomElementInterface.h"
@@ -95,8 +96,8 @@ public:
     void setShadowRoot(RefPtr<ShadowRoot>&& shadowRoot) { m_shadowRoot = WTFMove(shadowRoot); }
 
 #if ENABLE(CUSTOM_ELEMENTS)
-    JSCustomElementInterface* customElementInterface() { return m_customElementInterface.get(); }
-    void setCustomElementInterface(JSCustomElementInterface& customElementInterface) { m_customElementInterface = &customElementInterface; }
+    CustomElementReactionQueue* customElementReactionQueue() { return m_customElementReactionQueue.get(); }
+    void setCustomElementReactionQueue(std::unique_ptr<CustomElementReactionQueue>&& queue) { m_customElementReactionQueue = WTFMove(queue); }
 #endif
 
     NamedNodeMap* attributeMap() const { return m_attributeMap.get(); }
@@ -156,7 +157,7 @@ private:
     std::unique_ptr<DOMTokenList> m_classList;
     RefPtr<ShadowRoot> m_shadowRoot;
 #if ENABLE(CUSTOM_ELEMENTS)
-    RefPtr<JSCustomElementInterface> m_customElementInterface;
+    std::unique_ptr<CustomElementReactionQueue> m_customElementReactionQueue;
 #endif
     std::unique_ptr<NamedNodeMap> m_attributeMap;