CustomElementReactionQueue.cpp   [plain text]


/*
 * Copyright (C) 2015, 2016 Apple Inc. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL APPLE INC. OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
 * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

#include "config.h"
#include "CustomElementReactionQueue.h"

#include "CustomElementRegistry.h"
#include "DOMWindow.h"
#include "Document.h"
#include "Element.h"
#include "HTMLNames.h"
#include "JSCustomElementInterface.h"
#include "JSDOMBinding.h"
#include "Microtasks.h"
#include <JavaScriptCore/Heap.h>
#include <wtf/NeverDestroyed.h>
#include <wtf/Optional.h>
#include <wtf/Ref.h>
#include <wtf/SetForScope.h>

namespace WebCore {

class CustomElementReactionQueueItem {
public:
    enum class Type {
        ElementUpgrade,
        Connected,
        Disconnected,
        Adopted,
        AttributeChanged,
    };

    CustomElementReactionQueueItem(Type type)
        : m_type(type)
    { }

    CustomElementReactionQueueItem(Document& oldDocument, Document& newDocument)
        : m_type(Type::Adopted)
        , m_oldDocument(&oldDocument)
        , m_newDocument(&newDocument)
    { }

    CustomElementReactionQueueItem(const QualifiedName& attributeName, const AtomicString& oldValue, const AtomicString& newValue)
        : m_type(Type::AttributeChanged)
        , m_attributeName(attributeName)
        , m_oldValue(oldValue)
        , m_newValue(newValue)
    { }

    void invoke(Element& element, JSCustomElementInterface& elementInterface)
    {
        switch (m_type) {
        case Type::ElementUpgrade:
            elementInterface.upgradeElement(element);
            break;
        case Type::Connected:
            elementInterface.invokeConnectedCallback(element);
            break;
        case Type::Disconnected:
            elementInterface.invokeDisconnectedCallback(element);
            break;
        case Type::Adopted:
            elementInterface.invokeAdoptedCallback(element, *m_oldDocument, *m_newDocument);
            break;
        case Type::AttributeChanged:
            ASSERT(m_attributeName);
            elementInterface.invokeAttributeChangedCallback(element, m_attributeName.value(), m_oldValue, m_newValue);
            break;
        }
    }

private:
    Type m_type;
    RefPtr<Document> m_oldDocument;
    RefPtr<Document> m_newDocument;
    std::optional<QualifiedName> m_attributeName;
    AtomicString m_oldValue;
    AtomicString m_newValue;
};

CustomElementReactionQueue::CustomElementReactionQueue(JSCustomElementInterface& elementInterface)
    : m_interface(elementInterface)
{ }

CustomElementReactionQueue::~CustomElementReactionQueue()
{
    ASSERT(m_items.isEmpty());
}

void CustomElementReactionQueue::clear()
{
    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)
{
    ASSERT(element.isConnected());
    ASSERT(element.isCustomElementUpgradeCandidate());
    auto* window = element.document().domWindow();
    if (!window)
        return;

    auto* registry = window->customElementRegistry();
    if (!registry)
        return;

    auto* elementInterface = registry->findInterface(element);
    if (!elementInterface)
        return;

    element.enqueueToUpgrade(*elementInterface);
}

void CustomElementReactionQueue::enqueueConnectedCallbackIfNeeded(Element& element)
{
    ASSERT(element.isDefinedCustomElement());
    ASSERT(element.document().refCount() > 0);
    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());
    if (element.document().refCount() <= 0)
        return; // Don't enqueue disconnectedCallback if the entire document is getting destructed.
    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());
    ASSERT(element.document().refCount() > 0);
    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());
    ASSERT(element.document().refCount() > 0);
    auto& queue = CustomElementReactionStack::ensureCurrentQueue(element);
    if (queue.m_interface->observesAttribute(attributeName.localName()))
        queue.m_items.append({attributeName, oldValue, newValue});
}

void CustomElementReactionQueue::enqueuePostUpgradeReactions(Element& element)
{
    ASSERT(element.isCustomElementUpgradeCandidate());
    if (!element.hasAttributes() && !element.isConnected())
        return;

    auto* queue = element.reactionQueue();
    ASSERT(queue);

    if (element.hasAttributes()) {
        for (auto& attribute : element.attributesIterator()) {
            if (queue->m_interface->observesAttribute(attribute.localName()))
                queue->m_items.append({attribute.name(), nullAtom(), attribute.value()});
        }
    }

    if (element.isConnected() && queue->m_interface->hasConnectedCallback())
        queue->m_items.append({CustomElementReactionQueueItem::Type::Connected});
}

bool CustomElementReactionQueue::observesStyleAttribute() const
{
    return m_interface->observesAttribute(HTMLNames::styleAttr->localName());
}

void CustomElementReactionQueue::invokeAll(Element& element)
{
    while (!m_items.isEmpty()) {
        Vector<CustomElementReactionQueueItem> items = WTFMove(m_items);
        for (auto& item : items)
            item.invoke(element, m_interface.get());
    }
}

inline void CustomElementReactionStack::ElementQueue::add(Element& element)
{
    RELEASE_ASSERT(!m_invoking);
    // FIXME: Avoid inserting the same element multiple times.
    m_elements.append(element);
}

inline void CustomElementReactionStack::ElementQueue::invokeAll()
{
    RELEASE_ASSERT(!m_invoking);
    SetForScope<bool> invoking(m_invoking, true);
    Vector<Ref<Element>> elements;
    elements.swap(m_elements);
    RELEASE_ASSERT(m_elements.isEmpty());
    for (auto& element : elements) {
        auto* queue = element->reactionQueue();
        ASSERT(queue);
        queue->invokeAll(element.get());
    }
    RELEASE_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 ElementQueue;
    queue->add(element);
    return *element.reactionQueue();
}

CustomElementReactionStack* CustomElementReactionStack::s_currentProcessingStack = nullptr;

void CustomElementReactionStack::processQueue()
{
    ASSERT(m_queue);
    m_queue->invokeAll();
    delete m_queue;
    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();
}

}