/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

#include "MessagePort.h"

#include "MessageEvent.h"
#include "MessagePortChild.h"
#include "mozilla/dom/BlobBinding.h"
#include "mozilla/dom/Event.h"
#include "mozilla/dom/File.h"
#include "mozilla/dom/MessageChannel.h"
#include "mozilla/dom/MessageEventBinding.h"
#include "mozilla/dom/MessagePortBinding.h"
#include "mozilla/dom/MessagePortChild.h"
#include "mozilla/dom/PMessagePort.h"
#include "mozilla/dom/RootedDictionary.h"
#include "mozilla/dom/ScriptSettings.h"
#include "mozilla/dom/StructuredCloneTags.h"
#include "mozilla/dom/WorkerCommon.h"
#include "mozilla/dom/WorkerRef.h"
#include "mozilla/dom/WorkerScope.h"
#include "mozilla/ipc/BackgroundChild.h"
#include "mozilla/dom/RefMessageBodyService.h"
#include "mozilla/dom/SharedMessageBody.h"
#include "mozilla/ipc/PBackgroundChild.h"
#include "mozilla/MessagePortTimelineMarker.h"
#include "mozilla/ScopeExit.h"
#include "mozilla/TimelineConsumers.h"
#include "mozilla/TimelineMarker.h"
#include "mozilla/Unused.h"
#include "nsContentUtils.h"
#include "nsGlobalWindow.h"
#include "nsPresContext.h"

#ifdef XP_WIN
#  undef PostMessage
#endif

namespace mozilla::dom {

void UniqueMessagePortId::ForceClose() {
  if (!mIdentifier.neutered()) {
    MessagePort::ForceClose(mIdentifier);
    mIdentifier.neutered() = true;
  }
}

class PostMessageRunnable final : public CancelableRunnable {
  friend class MessagePort;

 public:
  PostMessageRunnable(MessagePort* aPort, SharedMessageBody* aData)
      : CancelableRunnable("dom::PostMessageRunnable"),
        mPort(aPort),
        mData(aData) {
    MOZ_ASSERT(aPort);
    MOZ_ASSERT(aData);
  }

  NS_IMETHOD
  Run() override {
    NS_ASSERT_OWNINGTHREAD(Runnable);

    // The port can be cycle collected while this runnable is pending in
    // the event queue.
    if (!mPort) {
      return NS_OK;
    }

    MOZ_ASSERT(mPort->mPostMessageRunnable == this);

    DispatchMessage();

    // We must check if we were waiting for this message in order to shutdown
    // the port.
    mPort->UpdateMustKeepAlive();

    mPort->mPostMessageRunnable = nullptr;
    mPort->Dispatch();

    return NS_OK;
  }

  nsresult Cancel() override {
    NS_ASSERT_OWNINGTHREAD(Runnable);

    mPort = nullptr;
    mData = nullptr;
    return NS_OK;
  }

 private:
  void DispatchMessage() const {
    NS_ASSERT_OWNINGTHREAD(Runnable);

    if (NS_FAILED(mPort->CheckCurrentGlobalCorrectness())) {
      return;
    }

    nsCOMPtr<nsIGlobalObject> globalObject = mPort->GetParentObject();

    AutoJSAPI jsapi;
    if (!globalObject || !jsapi.Init(globalObject)) {
      NS_WARNING("Failed to initialize AutoJSAPI object.");
      return;
    }

    JSContext* cx = jsapi.cx();

    IgnoredErrorResult rv;
    JS::Rooted<JS::Value> value(cx);

    UniquePtr<AbstractTimelineMarker> start;
    UniquePtr<AbstractTimelineMarker> end;
    bool isTimelineRecording = !TimelineConsumers::IsEmpty();

    if (isTimelineRecording) {
      start = MakeUnique<MessagePortTimelineMarker>(
          ProfileTimelineMessagePortOperationType::DeserializeData,
          MarkerTracingType::START);
    }

    mData->Read(cx, &value, mPort->mRefMessageBodyService,
                SharedMessageBody::ReadMethod::StealRefMessageBody, rv);

    if (isTimelineRecording) {
      end = MakeUnique<MessagePortTimelineMarker>(
          ProfileTimelineMessagePortOperationType::DeserializeData,
          MarkerTracingType::END);
      TimelineConsumers::AddMarkerForAllObservedDocShells(start);
      TimelineConsumers::AddMarkerForAllObservedDocShells(end);
    }

    if (NS_WARN_IF(rv.Failed())) {
      JS_ClearPendingException(cx);
      mPort->DispatchError();
      return;
    }

    // Create the event
    nsCOMPtr<mozilla::dom::EventTarget> eventTarget =
        do_QueryInterface(mPort->GetOwner());
    RefPtr<MessageEvent> event =
        new MessageEvent(eventTarget, nullptr, nullptr);

    Sequence<OwningNonNull<MessagePort>> ports;
    if (!mData->TakeTransferredPortsAsSequence(ports)) {
      mPort->DispatchError();
      return;
    }

    event->InitMessageEvent(nullptr, u"message"_ns, CanBubble::eNo,
                            Cancelable::eNo, value, u""_ns, u""_ns, nullptr,
                            ports);
    event->SetTrusted(true);

    mPort->DispatchEvent(*event);
  }

 private:
  ~PostMessageRunnable() = default;

  RefPtr<MessagePort> mPort;
  RefPtr<SharedMessageBody> mData;
};

NS_IMPL_CYCLE_COLLECTION_CLASS(MessagePort)

NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(MessagePort,
                                                DOMEventTargetHelper)
  if (tmp->mPostMessageRunnable) {
    NS_IMPL_CYCLE_COLLECTION_UNLINK(mPostMessageRunnable->mPort);
  }

  NS_IMPL_CYCLE_COLLECTION_UNLINK(mMessagesForTheOtherPort);

  tmp->CloseForced();
NS_IMPL_CYCLE_COLLECTION_UNLINK_END

NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(MessagePort,
                                                  DOMEventTargetHelper)
  if (tmp->mPostMessageRunnable) {
    NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mPostMessageRunnable->mPort);
  }

  NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mUnshippedEntangledPort);
NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END

NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(MessagePort)
NS_INTERFACE_MAP_END_INHERITING(DOMEventTargetHelper)

NS_IMPL_ADDREF_INHERITED(MessagePort, DOMEventTargetHelper)
NS_IMPL_RELEASE_INHERITED(MessagePort, DOMEventTargetHelper)

MessagePort::MessagePort(nsIGlobalObject* aGlobal, State aState)
    : DOMEventTargetHelper(aGlobal),
      mRefMessageBodyService(RefMessageBodyService::GetOrCreate()),
      mState(aState),
      mMessageQueueEnabled(false),
      mIsKeptAlive(false),
      mHasBeenTransferredOrClosed(false) {
  MOZ_ASSERT(aGlobal);

  mIdentifier = MakeUnique<MessagePortIdentifier>();
  mIdentifier->neutered() = true;
  mIdentifier->sequenceId() = 0;
}

MessagePort::~MessagePort() {
  CloseForced();
  MOZ_ASSERT(!mWorkerRef);
}

/* static */
already_AddRefed<MessagePort> MessagePort::Create(nsIGlobalObject* aGlobal,
                                                  const nsID& aUUID,
                                                  const nsID& aDestinationUUID,
                                                  ErrorResult& aRv) {
  MOZ_ASSERT(aGlobal);

  RefPtr<MessagePort> mp = new MessagePort(aGlobal, eStateUnshippedEntangled);
  mp->Initialize(aUUID, aDestinationUUID, 1 /* 0 is an invalid sequence ID */,
                 false /* Neutered */, aRv);
  return mp.forget();
}

/* static */
already_AddRefed<MessagePort> MessagePort::Create(
    nsIGlobalObject* aGlobal, UniqueMessagePortId& aIdentifier,
    ErrorResult& aRv) {
  MOZ_ASSERT(aGlobal);

  RefPtr<MessagePort> mp = new MessagePort(aGlobal, eStateEntangling);
  mp->Initialize(aIdentifier.uuid(), aIdentifier.destinationUuid(),
                 aIdentifier.sequenceId(), aIdentifier.neutered(), aRv);
  aIdentifier.neutered() = true;
  return mp.forget();
}

void MessagePort::UnshippedEntangle(MessagePort* aEntangledPort) {
  MOZ_DIAGNOSTIC_ASSERT(aEntangledPort);
  MOZ_DIAGNOSTIC_ASSERT(!mUnshippedEntangledPort);

  mUnshippedEntangledPort = aEntangledPort;
}

void MessagePort::Initialize(const nsID& aUUID, const nsID& aDestinationUUID,
                             uint32_t aSequenceID, bool aNeutered,
                             ErrorResult& aRv) {
  MOZ_ASSERT(mIdentifier);
  mIdentifier->uuid() = aUUID;
  mIdentifier->destinationUuid() = aDestinationUUID;
  mIdentifier->sequenceId() = aSequenceID;

  if (aNeutered) {
    // If this port is neutered we don't want to keep it alive artificially nor
    // we want to add listeners or WorkerRefs.
    mState = eStateDisentangled;
    return;
  }

  if (mState == eStateEntangling) {
    if (!ConnectToPBackground()) {
      aRv.Throw(NS_ERROR_FAILURE);
      return;
    }
  } else {
    MOZ_ASSERT(mState == eStateUnshippedEntangled);
  }

  // The port has to keep itself alive until it's entangled.
  UpdateMustKeepAlive();

  if (WorkerPrivate* workerPrivate = GetCurrentThreadWorkerPrivate()) {
    RefPtr<MessagePort> self = this;

    // When the callback is executed, we cannot process messages anymore because
    // we cannot dispatch new runnables. Let's force a Close().
    RefPtr<StrongWorkerRef> strongWorkerRef = StrongWorkerRef::Create(
        workerPrivate, "MessagePort", [self]() { self->CloseForced(); });
    if (NS_WARN_IF(!strongWorkerRef)) {
      // The worker is shutting down.
      mState = eStateDisentangled;
      UpdateMustKeepAlive();
      aRv.Throw(NS_ERROR_FAILURE);
      return;
    }

    MOZ_ASSERT(!mWorkerRef);
    mWorkerRef = std::move(strongWorkerRef);
  }
}

JSObject* MessagePort::WrapObject(JSContext* aCx,
                                  JS::Handle<JSObject*> aGivenProto) {
  return MessagePort_Binding::Wrap(aCx, this, aGivenProto);
}

void MessagePort::PostMessage(JSContext* aCx, JS::Handle<JS::Value> aMessage,
                              const Sequence<JSObject*>& aTransferable,
                              ErrorResult& aRv) {
  // We *must* clone the data here, or the JS::Value could be modified
  // by script

  // Here we want to check if the transerable object list contains
  // this port.
  for (uint32_t i = 0; i < aTransferable.Length(); ++i) {
    JS::Rooted<JSObject*> object(aCx, aTransferable[i]);
    if (!object) {
      continue;
    }

    MessagePort* port = nullptr;
    nsresult rv = UNWRAP_OBJECT(MessagePort, &object, port);
    if (NS_SUCCEEDED(rv) && port == this) {
      aRv.Throw(NS_ERROR_DOM_DATA_CLONE_ERR);
      return;
    }
  }

  JS::Rooted<JS::Value> transferable(aCx, JS::UndefinedValue());

  aRv = nsContentUtils::CreateJSValueFromSequenceOfObject(aCx, aTransferable,
                                                          &transferable);
  if (NS_WARN_IF(aRv.Failed())) {
    return;
  }

  Maybe<nsID> agentClusterId;
  nsCOMPtr<nsIGlobalObject> global = GetOwnerGlobal();
  if (global) {
    agentClusterId = global->GetAgentClusterId();
  }

  RefPtr<SharedMessageBody> data = new SharedMessageBody(
      StructuredCloneHolder::TransferringSupported, agentClusterId);

  UniquePtr<AbstractTimelineMarker> start;
  UniquePtr<AbstractTimelineMarker> end;
  bool isTimelineRecording = !TimelineConsumers::IsEmpty();

  if (isTimelineRecording) {
    start = MakeUnique<MessagePortTimelineMarker>(
        ProfileTimelineMessagePortOperationType::SerializeData,
        MarkerTracingType::START);
  }

  data->Write(aCx, aMessage, transferable, mIdentifier->uuid(),
              mRefMessageBodyService, aRv);

  if (isTimelineRecording) {
    end = MakeUnique<MessagePortTimelineMarker>(
        ProfileTimelineMessagePortOperationType::SerializeData,
        MarkerTracingType::END);
    TimelineConsumers::AddMarkerForAllObservedDocShells(start);
    TimelineConsumers::AddMarkerForAllObservedDocShells(end);
  }

  if (NS_WARN_IF(aRv.Failed())) {
    return;
  }

  // This message has to be ignored.
  if (mState > eStateEntangled) {
    return;
  }

  // If we are unshipped we are connected to the other port on the same thread.
  if (mState == eStateUnshippedEntangled) {
    MOZ_DIAGNOSTIC_ASSERT(mUnshippedEntangledPort);
    mUnshippedEntangledPort->mMessages.AppendElement(data);
    mUnshippedEntangledPort->Dispatch();
    return;
  }

  // Not entangled yet, but already closed/disentangled.
  if (mState == eStateEntanglingForDisentangle ||
      mState == eStateEntanglingForClose) {
    return;
  }

  RemoveDocFromBFCache();

  // Not entangled yet.
  if (mState == eStateEntangling) {
    mMessagesForTheOtherPort.AppendElement(data);
    return;
  }

  MOZ_ASSERT(mActor);
  MOZ_ASSERT(mMessagesForTheOtherPort.IsEmpty());

  AutoTArray<RefPtr<SharedMessageBody>, 1> array;
  array.AppendElement(data);

  AutoTArray<MessageData, 1> messages;
  // note: `messages` will borrow the underlying buffer, but this is okay
  // because reverse destruction order means `messages` will be destroyed prior
  // to `array`/`data`.
  SharedMessageBody::FromSharedToMessagesChild(mActor->Manager(), array,
                                               messages);
  mActor->SendPostMessages(messages);
}

void MessagePort::PostMessage(JSContext* aCx, JS::Handle<JS::Value> aMessage,
                              const StructuredSerializeOptions& aOptions,
                              ErrorResult& aRv) {
  PostMessage(aCx, aMessage, aOptions.mTransfer, aRv);
}

void MessagePort::Start() {
  if (mMessageQueueEnabled) {
    return;
  }

  mMessageQueueEnabled = true;
  Dispatch();
}

void MessagePort::Dispatch() {
  if (!mMessageQueueEnabled || mMessages.IsEmpty() || mPostMessageRunnable) {
    return;
  }

  switch (mState) {
    case eStateUnshippedEntangled:
      // Everything is fine here. We have messages because the other
      // port populates our queue directly.
      break;

    case eStateEntangling:
      // Everything is fine here as well. We have messages because the other
      // port populated our queue directly when we were in the
      // eStateUnshippedEntangled state.
      break;

    case eStateEntanglingForDisentangle:
      // Here we don't want to ship messages because these messages must be
      // delivered by the cloned version of this one. They will be sent in the
      // SendDisentangle().
      return;

    case eStateEntanglingForClose:
      // We still want to deliver messages if we are closing. These messages
      // are here from the previous eStateUnshippedEntangled state.
      break;

    case eStateEntangled:
      // This port is up and running.
      break;

    case eStateDisentangling:
      // If we are in the process to disentangle the port, we cannot dispatch
      // messages. They will be sent to the cloned version of this port via
      // SendDisentangle();
      return;

    case eStateDisentangled:
      MOZ_CRASH("This cannot happen.");
      // It cannot happen because Disentangle should take off all the pending
      // messages.
      break;

    case eStateDisentangledForClose:
      // If we are here is because the port has been closed. We can still
      // process the pending messages.
      break;
  }

  RefPtr<SharedMessageBody> data = mMessages.ElementAt(0);
  mMessages.RemoveElementAt(0);

  mPostMessageRunnable = new PostMessageRunnable(this, data);

  nsCOMPtr<nsIGlobalObject> global = GetOwnerGlobal();
  if (NS_IsMainThread() && global) {
    MOZ_ALWAYS_SUCCEEDS(
        global->Dispatch(TaskCategory::Other, do_AddRef(mPostMessageRunnable)));
    return;
  }

  MOZ_ALWAYS_SUCCEEDS(NS_DispatchToCurrentThread(mPostMessageRunnable));
}

void MessagePort::Close() {
  mHasBeenTransferredOrClosed = true;
  CloseInternal(true /* aSoftly */);
}

void MessagePort::CloseForced() { CloseInternal(false /* aSoftly */); }

void MessagePort::CloseInternal(bool aSoftly) {
  // If we have some messages to send but we don't want a 'soft' close, we have
  // to flush them now.
  if (!aSoftly) {
    mMessages.Clear();
  }

  // Let's inform the RefMessageBodyService that any our shared messages are
  // now invalid.
  mRefMessageBodyService->ForgetPort(mIdentifier->uuid());

  if (mState == eStateUnshippedEntangled) {
    MOZ_DIAGNOSTIC_ASSERT(mUnshippedEntangledPort);

    // This avoids loops.
    RefPtr<MessagePort> port = std::move(mUnshippedEntangledPort);

    mState = eStateDisentangledForClose;
    port->CloseInternal(aSoftly);

    UpdateMustKeepAlive();
    return;
  }

  // Not entangled yet, we have to wait.
  if (mState == eStateEntangling) {
    mState = eStateEntanglingForClose;
    return;
  }

  // Not entangled but already cloned or closed
  if (mState == eStateEntanglingForDisentangle ||
      mState == eStateEntanglingForClose) {
    return;
  }

  // Maybe we were already closing the port but softly. In this case we call
  // UpdateMustKeepAlive() to consider the empty pending message queue.
  if (mState == eStateDisentangledForClose && !aSoftly) {
    UpdateMustKeepAlive();
    return;
  }

  if (mState > eStateEntangled) {
    return;
  }

  // We don't care about stopping the sending of messages because from now all
  // the incoming messages will be ignored.
  mState = eStateDisentangledForClose;

  MOZ_ASSERT(mActor);

  mActor->SendClose();
  mActor->SetPort(nullptr);
  mActor = nullptr;

  UpdateMustKeepAlive();
}

EventHandlerNonNull* MessagePort::GetOnmessage() {
  return GetEventHandler(nsGkAtoms::onmessage);
}

void MessagePort::SetOnmessage(EventHandlerNonNull* aCallback) {
  SetEventHandler(nsGkAtoms::onmessage, aCallback);

  // When using onmessage, the call to start() is implied.
  Start();
}

// This method is called when the PMessagePortChild actor is entangled to
// another actor. It receives a list of messages to be dispatch. It can be that
// we were waiting for this entangling step in order to disentangle the port or
// to close it.
void MessagePort::Entangled(nsTArray<MessageData>& aMessages) {
  MOZ_ASSERT(mState == eStateEntangling ||
             mState == eStateEntanglingForDisentangle ||
             mState == eStateEntanglingForClose);

  State oldState = mState;
  mState = eStateEntangled;

  // If we have pending messages, these have to be sent.
  if (!mMessagesForTheOtherPort.IsEmpty()) {
    {
      nsTArray<MessageData> messages;
      SharedMessageBody::FromSharedToMessagesChild(
          mActor->Manager(), mMessagesForTheOtherPort, messages);
      mActor->SendPostMessages(messages);
    }
    // Because `messages` borrow the underlying JSStructuredCloneData buffers,
    // only clear after `messages` have gone out of scope.
    mMessagesForTheOtherPort.Clear();
  }

  // We must convert the messages into SharedMessageBodys to avoid leaks.
  FallibleTArray<RefPtr<SharedMessageBody>> data;
  if (NS_WARN_IF(
          !SharedMessageBody::FromMessagesToSharedChild(aMessages, data))) {
    DispatchError();
    return;
  }

  // If the next step is to close the port, we do it ignoring the received
  // messages.
  if (oldState == eStateEntanglingForClose) {
    CloseForced();
    return;
  }

  mMessages.AppendElements(data);

  // We were waiting for the entangling callback in order to disentangle this
  // port immediately after.
  if (oldState == eStateEntanglingForDisentangle) {
    StartDisentangling();
    return;
  }

  Dispatch();
}

void MessagePort::StartDisentangling() {
  MOZ_ASSERT(mActor);
  MOZ_ASSERT(mState == eStateEntangled);

  mState = eStateDisentangling;

  // Sending this message we communicate to the parent actor that we don't want
  // to receive any new messages. It is possible that a message has been
  // already sent but not received yet. So we have to collect all of them and
  // we send them in the SendDispatch() request.
  mActor->SendStopSendingData();
}

void MessagePort::MessagesReceived(nsTArray<MessageData>& aMessages) {
  MOZ_ASSERT(mState == eStateEntangled || mState == eStateDisentangling ||
             // This last step can happen only if Close() has been called
             // manually. At this point SendClose() is sent but we can still
             // receive something until the Closing request is processed.
             mState == eStateDisentangledForClose);
  MOZ_ASSERT(mMessagesForTheOtherPort.IsEmpty());

  RemoveDocFromBFCache();

  FallibleTArray<RefPtr<SharedMessageBody>> data;
  if (NS_WARN_IF(
          !SharedMessageBody::FromMessagesToSharedChild(aMessages, data))) {
    DispatchError();
    return;
  }

  mMessages.AppendElements(data);

  if (mState == eStateEntangled) {
    Dispatch();
  }
}

void MessagePort::StopSendingDataConfirmed() {
  MOZ_ASSERT(mState == eStateDisentangling);
  MOZ_ASSERT(mActor);

  Disentangle();
}

void MessagePort::Disentangle() {
  MOZ_ASSERT(mState == eStateDisentangling);
  MOZ_ASSERT(mActor);

  mState = eStateDisentangled;

  {
    nsTArray<MessageData> messages;
    SharedMessageBody::FromSharedToMessagesChild(mActor->Manager(), mMessages,
                                                 messages);
    mActor->SendDisentangle(messages);
  }

  // Let's inform the RefMessageBodyService that any our shared messages are
  // now invalid.
  mRefMessageBodyService->ForgetPort(mIdentifier->uuid());

  // Only clear mMessages after the MessageData instances have gone out of scope
  // because they borrow mMessages' underlying JSStructuredCloneDatas.
  mMessages.Clear();

  mActor->SetPort(nullptr);
  mActor = nullptr;

  UpdateMustKeepAlive();
}

void MessagePort::CloneAndDisentangle(UniqueMessagePortId& aIdentifier) {
  MOZ_ASSERT(mIdentifier);
  MOZ_ASSERT(!mHasBeenTransferredOrClosed);

  mHasBeenTransferredOrClosed = true;

  // We can clone a port that has already been transfered. In this case, on the
  // otherside will have a neutered port. Here we set neutered to true so that
  // we are safe in case a early return.
  aIdentifier.neutered() = true;

  if (mState > eStateEntangled) {
    return;
  }

  // We already have a 'next step'. We have to consider this port as already
  // cloned/closed/disentangled.
  if (mState == eStateEntanglingForDisentangle ||
      mState == eStateEntanglingForClose) {
    return;
  }

  aIdentifier.uuid() = mIdentifier->uuid();
  aIdentifier.destinationUuid() = mIdentifier->destinationUuid();
  aIdentifier.sequenceId() = mIdentifier->sequenceId() + 1;
  aIdentifier.neutered() = false;

  // We have to entangle first.
  if (mState == eStateUnshippedEntangled) {
    MOZ_ASSERT(mUnshippedEntangledPort);
    MOZ_ASSERT(mMessagesForTheOtherPort.IsEmpty());

    RefPtr<MessagePort> port = std::move(mUnshippedEntangledPort);

    // Disconnect the entangled port and connect it to PBackground.
    if (!port->ConnectToPBackground()) {
      // We are probably shutting down. We cannot proceed.
      mState = eStateDisentangled;
      UpdateMustKeepAlive();
      return;
    }

    // In this case, we don't need to be connected to the PBackground service.
    if (mMessages.IsEmpty()) {
      aIdentifier.sequenceId() = mIdentifier->sequenceId();

      mState = eStateDisentangled;
      UpdateMustKeepAlive();
      return;
    }

    // Register this component to PBackground.
    if (!ConnectToPBackground()) {
      // We are probably shutting down. We cannot proceed.
      return;
    }

    mState = eStateEntanglingForDisentangle;
    return;
  }

  // Not entangled yet, we have to wait.
  if (mState == eStateEntangling) {
    mState = eStateEntanglingForDisentangle;
    return;
  }

  MOZ_ASSERT(mState == eStateEntangled);
  StartDisentangling();
}

void MessagePort::Closed() {
  if (mState >= eStateDisentangled) {
    return;
  }

  mState = eStateDisentangledForClose;

  if (mActor) {
    mActor->SetPort(nullptr);
    mActor = nullptr;
  }

  UpdateMustKeepAlive();
}

bool MessagePort::ConnectToPBackground() {
  RefPtr<MessagePort> self = this;
  auto raii = MakeScopeExit([self] {
    self->mState = eStateDisentangled;
    self->UpdateMustKeepAlive();
  });

  mozilla::ipc::PBackgroundChild* actorChild =
      mozilla::ipc::BackgroundChild::GetOrCreateForCurrentThread();
  if (NS_WARN_IF(!actorChild)) {
    return false;
  }

  PMessagePortChild* actor = actorChild->SendPMessagePortConstructor(
      mIdentifier->uuid(), mIdentifier->destinationUuid(),
      mIdentifier->sequenceId());
  if (NS_WARN_IF(!actor)) {
    return false;
  }

  mActor = static_cast<MessagePortChild*>(actor);
  MOZ_ASSERT(mActor);

  mActor->SetPort(this);
  mState = eStateEntangling;

  raii.release();
  return true;
}

void MessagePort::UpdateMustKeepAlive() {
  if (mState >= eStateDisentangled && mMessages.IsEmpty() && mIsKeptAlive) {
    mIsKeptAlive = false;

    // The DTOR of this WorkerRef will release the worker for us.
    mWorkerRef = nullptr;

    Release();
    return;
  }

  if (mState < eStateDisentangled && !mIsKeptAlive) {
    mIsKeptAlive = true;
    AddRef();
  }
}

void MessagePort::DisconnectFromOwner() {
  CloseForced();
  DOMEventTargetHelper::DisconnectFromOwner();
}

void MessagePort::RemoveDocFromBFCache() {
  if (!NS_IsMainThread()) {
    return;
  }

  if (nsPIDOMWindowInner* window = GetOwner()) {
    window->RemoveFromBFCacheSync();
  }
}

/* static */
void MessagePort::ForceClose(const MessagePortIdentifier& aIdentifier) {
  mozilla::ipc::PBackgroundChild* actorChild =
      mozilla::ipc::BackgroundChild::GetOrCreateForCurrentThread();
  if (NS_WARN_IF(!actorChild)) {
    MOZ_CRASH("Failed to create a PBackgroundChild actor!");
  }

  Unused << actorChild->SendMessagePortForceClose(aIdentifier.uuid(),
                                                  aIdentifier.destinationUuid(),
                                                  aIdentifier.sequenceId());
}

void MessagePort::DispatchError() {
  nsCOMPtr<nsIGlobalObject> globalObject = GetParentObject();

  AutoJSAPI jsapi;
  if (!globalObject || !jsapi.Init(globalObject)) {
    NS_WARNING("Failed to initialize AutoJSAPI object.");
    return;
  }

  RootedDictionary<MessageEventInit> init(jsapi.cx());
  init.mBubbles = false;
  init.mCancelable = false;

  RefPtr<Event> event =
      MessageEvent::Constructor(this, u"messageerror"_ns, init);
  event->SetTrusted(true);

  DispatchEvent(*event);
}

}  // namespace mozilla::dom
