/* -*- 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 "mozilla/ArrayUtils.h"
#include "mozilla/Components.h"
#include "mozilla/DataMutex.h"
#include "mozilla/DebugOnly.h"
#include "mozilla/LoadInfo.h"
#include "mozilla/DOMEventTargetHelper.h"
#include "mozilla/dom/EventSource.h"
#include "mozilla/dom/EventSourceBinding.h"
#include "mozilla/dom/MessageEvent.h"
#include "mozilla/dom/MessageEventBinding.h"
#include "mozilla/dom/ScriptSettings.h"
#include "mozilla/dom/WorkerPrivate.h"
#include "mozilla/dom/WorkerRef.h"
#include "mozilla/dom/WorkerRunnable.h"
#include "mozilla/dom/WorkerScope.h"
#include "mozilla/dom/EventSourceEventService.h"
#include "mozilla/ScopeExit.h"
#include "mozilla/UniquePtrExtensions.h"
#include "nsComponentManagerUtils.h"
#include "nsIThreadRetargetableStreamListener.h"
#include "nsNetUtil.h"
#include "nsIAuthPrompt.h"
#include "nsIAuthPrompt2.h"
#include "nsIHttpChannel.h"
#include "nsIInputStream.h"
#include "nsIInterfaceRequestorUtils.h"
#include "nsMimeTypes.h"
#include "nsIPromptFactory.h"
#include "nsIWindowWatcher.h"
#include "nsPresContext.h"
#include "nsProxyRelease.h"
#include "nsContentPolicyUtils.h"
#include "nsIStringBundle.h"
#include "nsIConsoleService.h"
#include "nsIObserverService.h"
#include "nsIScriptObjectPrincipal.h"
#include "nsJSUtils.h"
#include "nsIThreadRetargetableRequest.h"
#include "nsIAsyncVerifyRedirectCallback.h"
#include "nsIScriptError.h"
#include "nsContentUtils.h"
#include "mozilla/Preferences.h"
#include "xpcpublic.h"
#include "nsWrapperCacheInlines.h"
#include "mozilla/Attributes.h"
#include "nsError.h"
#include "mozilla/Encoding.h"
#include "ReferrerInfo.h"

namespace mozilla::dom {

#ifdef DEBUG
static LazyLogModule gEventSourceLog("EventSource");
#endif

#define SPACE_CHAR (char16_t)0x0020
#define CR_CHAR (char16_t)0x000D
#define LF_CHAR (char16_t)0x000A
#define COLON_CHAR (char16_t)0x003A

// Reconnection time related values in milliseconds. The default one is equal
// to the default value of the pref dom.server-events.default-reconnection-time
#define MIN_RECONNECTION_TIME_VALUE 500
#define DEFAULT_RECONNECTION_TIME_VALUE 5000
#define MAX_RECONNECTION_TIME_VALUE \
  PR_IntervalToMilliseconds(DELAY_INTERVAL_LIMIT)

class EventSourceImpl final : public nsIObserver,
                              public nsIStreamListener,
                              public nsIChannelEventSink,
                              public nsIInterfaceRequestor,
                              public nsSupportsWeakReference,
                              public nsISerialEventTarget,
                              public nsITimerCallback,
                              public nsINamed,
                              public nsIThreadRetargetableStreamListener {
 public:
  NS_DECL_THREADSAFE_ISUPPORTS
  NS_DECL_NSIOBSERVER
  NS_DECL_NSIREQUESTOBSERVER
  NS_DECL_NSISTREAMLISTENER
  NS_DECL_NSICHANNELEVENTSINK
  NS_DECL_NSIINTERFACEREQUESTOR
  NS_DECL_NSIEVENTTARGET_FULL
  NS_DECL_NSITIMERCALLBACK
  NS_DECL_NSINAMED
  NS_DECL_NSITHREADRETARGETABLESTREAMLISTENER

  EventSourceImpl(EventSource* aEventSource,
                  nsICookieJarSettings* aCookieJarSettings);

  enum { CONNECTING = 0U, OPEN = 1U, CLOSED = 2U };

  void Close();

  void Init(nsIPrincipal* aPrincipal, const nsAString& aURL, ErrorResult& aRv);

  nsresult GetBaseURI(nsIURI** aBaseURI);

  void SetupHttpChannel();
  nsresult SetupReferrerInfo(const nsCOMPtr<Document>& aDocument);
  nsresult InitChannelAndRequestEventSource(bool aEventTargetAccessAllowed);
  nsresult ResetConnection();
  void ResetDecoder();
  nsresult SetReconnectionTimeout();

  void AnnounceConnection();
  void DispatchAllMessageEvents();
  nsresult RestartConnection();
  void ReestablishConnection();
  void DispatchFailConnection();
  void FailConnection();

  nsresult Thaw();
  nsresult Freeze();

  nsresult PrintErrorOnConsole(const char* aBundleURI, const char* aError,
                               const nsTArray<nsString>& aFormatStrings);
  nsresult ConsoleError();

  static nsresult StreamReaderFunc(nsIInputStream* aInputStream, void* aClosure,
                                   const char* aFromRawSegment,
                                   uint32_t aToOffset, uint32_t aCount,
                                   uint32_t* aWriteCount);
  void ParseSegment(const char* aBuffer, uint32_t aLength);
  nsresult SetFieldAndClear();
  void ClearFields();
  nsresult ResetEvent();
  nsresult DispatchCurrentMessageEvent();
  nsresult ParseCharacter(char16_t aChr);
  nsresult CheckHealthOfRequestCallback(nsIRequest* aRequestCallback);
  nsresult OnRedirectVerifyCallback(nsresult result);
  nsresult ParseURL(const nsAString& aURL);
  nsresult AddWindowObservers();
  void RemoveWindowObservers();

  void CloseInternal();
  void CleanupOnMainThread();

  bool CreateWorkerRef(WorkerPrivate* aWorkerPrivate);
  void ReleaseWorkerRef();

  void AssertIsOnTargetThread() const {
    MOZ_DIAGNOSTIC_ASSERT(IsTargetThread());
  }

  bool IsTargetThread() const { return NS_GetCurrentThread() == mTargetThread; }

  uint16_t ReadyState() {
    auto lock = mSharedData.Lock();
    if (lock->mEventSource) {
      return lock->mEventSource->mReadyState;
    }
    // EventSourceImpl keeps EventSource alive. If mEventSource is null, it
    // means that the EventSource has been closed.
    return CLOSED;
  }

  void SetReadyState(uint16_t aReadyState) {
    auto lock = mSharedData.Lock();
    MOZ_ASSERT(lock->mEventSource);
    MOZ_ASSERT(!mIsShutDown);
    lock->mEventSource->mReadyState = aReadyState;
  }

  bool IsClosed() { return ReadyState() == CLOSED; }

  RefPtr<EventSource> GetEventSource() {
    AssertIsOnTargetThread();
    auto lock = mSharedData.Lock();
    return lock->mEventSource;
  }

  /**
   * A simple state machine used to manage the event-source's line buffer
   *
   * PARSE_STATE_OFF              -> PARSE_STATE_BEGIN_OF_STREAM
   *
   * PARSE_STATE_BEGIN_OF_STREAM     -> PARSE_STATE_CR_CHAR |
   *                                 PARSE_STATE_BEGIN_OF_LINE |
   *                                 PARSE_STATE_COMMENT |
   *                                 PARSE_STATE_FIELD_NAME
   *
   * PARSE_STATE_CR_CHAR -> PARSE_STATE_CR_CHAR |
   *                        PARSE_STATE_COMMENT |
   *                        PARSE_STATE_FIELD_NAME |
   *                        PARSE_STATE_BEGIN_OF_LINE
   *
   * PARSE_STATE_COMMENT -> PARSE_STATE_CR_CHAR |
   *                        PARSE_STATE_BEGIN_OF_LINE
   *
   * PARSE_STATE_FIELD_NAME   -> PARSE_STATE_CR_CHAR |
   *                             PARSE_STATE_BEGIN_OF_LINE |
   *                             PARSE_STATE_FIRST_CHAR_OF_FIELD_VALUE
   *
   * PARSE_STATE_FIRST_CHAR_OF_FIELD_VALUE  -> PARSE_STATE_FIELD_VALUE |
   *                                           PARSE_STATE_CR_CHAR |
   *                                           PARSE_STATE_BEGIN_OF_LINE
   *
   * PARSE_STATE_FIELD_VALUE      -> PARSE_STATE_CR_CHAR |
   *                                 PARSE_STATE_BEGIN_OF_LINE
   *
   * PARSE_STATE_BEGIN_OF_LINE   -> PARSE_STATE_CR_CHAR |
   *                                PARSE_STATE_COMMENT |
   *                                PARSE_STATE_FIELD_NAME |
   *                                PARSE_STATE_BEGIN_OF_LINE
   *
   * Whenever the parser find an empty line or the end-of-file
   * it dispatches the stacked event.
   *
   */
  enum ParserStatus {
    PARSE_STATE_OFF = 0,
    PARSE_STATE_BEGIN_OF_STREAM,
    PARSE_STATE_CR_CHAR,
    PARSE_STATE_COMMENT,
    PARSE_STATE_FIELD_NAME,
    PARSE_STATE_FIRST_CHAR_OF_FIELD_VALUE,
    PARSE_STATE_FIELD_VALUE,
    PARSE_STATE_IGNORE_FIELD_VALUE,
    PARSE_STATE_BEGIN_OF_LINE
  };

  // Connection related data members. Should only be accessed on main thread.
  nsCOMPtr<nsIURI> mSrc;
  uint32_t mReconnectionTime;  // in ms
  nsCOMPtr<nsIPrincipal> mPrincipal;
  nsString mOrigin;
  nsCOMPtr<nsITimer> mTimer;
  nsCOMPtr<nsIHttpChannel> mHttpChannel;

  struct Message {
    nsString mEventName;
    // We need to be able to distinguish between different states of id field:
    // 1) is not given at all
    // 2) is given but is empty
    // 3) is given and has a value
    // We can't check for the 1st state with a simple nsString.
    Maybe<nsString> mLastEventID;
    nsString mData;
  };

  // Message related data members. May be set / initialized when initializing
  // EventSourceImpl on target thread but should only be used on target thread.
  nsString mLastEventID;
  UniquePtr<Message> mCurrentMessage;
  nsDeque<Message> mMessagesToDispatch;
  ParserStatus mStatus;
  mozilla::UniquePtr<mozilla::Decoder> mUnicodeDecoder;
  nsString mLastFieldName;
  nsString mLastFieldValue;

  // EventSourceImpl internal states.
  // WorkerRef to keep the worker alive. (accessed on worker thread only)
  RefPtr<ThreadSafeWorkerRef> mWorkerRef;
  // Whether the window is frozen. May be set on main thread and read on target
  // thread.
  Atomic<bool> mFrozen;
  // There are some messages are going to be dispatched when thaw.
  bool mGoingToDispatchAllMessages;
  // Whether the EventSource is run on main thread.
  const bool mIsMainThread;
  // Whether the EventSourceImpl is going to be destroyed.
  Atomic<bool> mIsShutDown;

  class EventSourceServiceNotifier final {
   public:
    EventSourceServiceNotifier(RefPtr<EventSourceImpl>&& aEventSourceImpl,
                               uint64_t aHttpChannelId, uint64_t aInnerWindowID)
        : mEventSourceImpl(std::move(aEventSourceImpl)),
          mHttpChannelId(aHttpChannelId),
          mInnerWindowID(aInnerWindowID),
          mConnectionOpened(false) {
      AssertIsOnMainThread();
      mService = EventSourceEventService::GetOrCreate();
    }

    void ConnectionOpened() {
      mEventSourceImpl->AssertIsOnTargetThread();
      mService->EventSourceConnectionOpened(mHttpChannelId, mInnerWindowID);
      mConnectionOpened = true;
    }

    void EventReceived(const nsAString& aEventName,
                       const nsAString& aLastEventID, const nsAString& aData,
                       uint32_t aRetry, DOMHighResTimeStamp aTimeStamp) {
      mEventSourceImpl->AssertIsOnTargetThread();
      mService->EventReceived(mHttpChannelId, mInnerWindowID, aEventName,
                              aLastEventID, aData, aRetry, aTimeStamp);
    }

    ~EventSourceServiceNotifier() {
      // It is safe to call this on any thread because
      // EventSourceConnectionClosed method is thread safe and
      // NS_ReleaseOnMainThread explicitly releases the service on the main
      // thread.
      if (mConnectionOpened) {
        // We want to notify about connection being closed only if we told
        // it was ever opened. The check is needed if OnStartRequest is called
        // on the main thread while close() is called on a worker thread.
        mService->EventSourceConnectionClosed(mHttpChannelId, mInnerWindowID);
      }
      NS_ReleaseOnMainThread("EventSourceServiceNotifier::mService",
                             mService.forget());
    }

   private:
    RefPtr<EventSourceEventService> mService;
    RefPtr<EventSourceImpl> mEventSourceImpl;
    uint64_t mHttpChannelId;
    uint64_t mInnerWindowID;
    bool mConnectionOpened;
  };

  struct SharedData {
    RefPtr<EventSource> mEventSource;
    UniquePtr<EventSourceServiceNotifier> mServiceNotifier;
  };

  DataMutex<SharedData> mSharedData;

  // Event Source owner information:
  // - the script file name
  // - source code line number and column number where the Event Source object
  //   was constructed.
  // - the ID of the inner window where the script lives. Note that this may not
  //   be the same as the Event Source owner window.
  // These attributes are used for error reporting. Should only be accessed on
  // target thread
  nsString mScriptFile;
  uint32_t mScriptLine;
  uint32_t mScriptColumn;
  uint64_t mInnerWindowID;

 private:
  nsCOMPtr<nsICookieJarSettings> mCookieJarSettings;

  // Pointer to the target thread for checking whether we are
  // on the target thread. This is intentionally a non-owning
  // pointer in order not to affect the thread destruction
  // sequence. This pointer must only be compared for equality
  // and must not be dereferenced.
  nsIThread* mTargetThread;

  // prevent bad usage
  EventSourceImpl(const EventSourceImpl& x) = delete;
  EventSourceImpl& operator=(const EventSourceImpl& x) = delete;
  ~EventSourceImpl() {
    if (IsClosed()) {
      return;
    }
    // If we threw during Init we never called Close
    SetReadyState(CLOSED);
    CloseInternal();
  }
};

NS_IMPL_ISUPPORTS(EventSourceImpl, nsIObserver, nsIStreamListener,
                  nsIRequestObserver, nsIChannelEventSink,
                  nsIInterfaceRequestor, nsISupportsWeakReference,
                  nsISerialEventTarget, nsIEventTarget,
                  nsIThreadRetargetableStreamListener, nsITimerCallback,
                  nsINamed)

EventSourceImpl::EventSourceImpl(EventSource* aEventSource,
                                 nsICookieJarSettings* aCookieJarSettings)
    : mReconnectionTime(0),
      mStatus(PARSE_STATE_OFF),
      mFrozen(false),
      mGoingToDispatchAllMessages(false),
      mIsMainThread(NS_IsMainThread()),
      mIsShutDown(false),
      mSharedData(SharedData{aEventSource}, "EventSourceImpl::mSharedData"),
      mScriptLine(0),
      mScriptColumn(0),
      mInnerWindowID(0),
      mCookieJarSettings(aCookieJarSettings),
      mTargetThread(NS_GetCurrentThread()) {
  MOZ_ASSERT(aEventSource);
  SetReadyState(CONNECTING);
}

class CleanupRunnable final : public WorkerMainThreadRunnable {
 public:
  explicit CleanupRunnable(RefPtr<EventSourceImpl>&& aEventSourceImpl)
      : WorkerMainThreadRunnable(GetCurrentThreadWorkerPrivate(),
                                 "EventSource :: Cleanup"_ns),
        mESImpl(std::move(aEventSourceImpl)) {
    MOZ_ASSERT(mESImpl);
    mWorkerPrivate->AssertIsOnWorkerThread();
  }

  bool MainThreadRun() override {
    MOZ_ASSERT(mESImpl);
    mESImpl->CleanupOnMainThread();
    // We want to ensure the shortest possible remaining lifetime
    // and not depend on the Runnable's destruction.
    mESImpl = nullptr;
    return true;
  }

 protected:
  RefPtr<EventSourceImpl> mESImpl;
};

void EventSourceImpl::Close() {
  if (IsClosed()) {
    return;
  }

  SetReadyState(CLOSED);
  // CloseInternal potentially kills ourself, ensure
  // to not access any members afterwards.
  CloseInternal();
}

void EventSourceImpl::CloseInternal() {
  AssertIsOnTargetThread();
  MOZ_ASSERT(IsClosed());

  RefPtr<EventSource> myES;
  {
    auto lock = mSharedData.Lock();
    // We want to ensure to release ourself even if we have
    // the shutdown case, thus we put aside a pointer
    // to the EventSource and null it out right now.
    myES = std::move(lock->mEventSource);
    lock->mEventSource = nullptr;
    lock->mServiceNotifier = nullptr;
  }

  MOZ_ASSERT(!mIsShutDown);
  if (mIsShutDown) {
    return;
  }

  // Invoke CleanupOnMainThread before cleaning any members. It will call
  // ShutDown, which is supposed to be called before cleaning any members.
  if (NS_IsMainThread()) {
    CleanupOnMainThread();
  } else {
    ErrorResult rv;
    // run CleanupOnMainThread synchronously on main thread since it touches
    // observers and members only can be accessed on main thread.
    RefPtr<CleanupRunnable> runnable = new CleanupRunnable(this);
    runnable->Dispatch(Killing, rv);
    MOZ_ASSERT(!rv.Failed());
    ReleaseWorkerRef();
  }

  while (mMessagesToDispatch.GetSize() != 0) {
    delete mMessagesToDispatch.PopFront();
  }
  mFrozen = false;
  ResetDecoder();
  mUnicodeDecoder = nullptr;
  // Release the object on its owner. Don't access to any members
  // after it.
  myES->mESImpl = nullptr;
}

void EventSourceImpl::CleanupOnMainThread() {
  AssertIsOnMainThread();
  MOZ_ASSERT(IsClosed());

  // Call ShutDown before cleaning any members.
  MOZ_ASSERT(!mIsShutDown);
  mIsShutDown = true;

  if (mIsMainThread) {
    RemoveWindowObservers();
  }

  if (mTimer) {
    mTimer->Cancel();
    mTimer = nullptr;
  }

  ResetConnection();
  mPrincipal = nullptr;
  mSrc = nullptr;
}

class InitRunnable final : public WorkerMainThreadRunnable {
 public:
  InitRunnable(WorkerPrivate* aWorkerPrivate,
               RefPtr<EventSourceImpl> aEventSourceImpl, const nsAString& aURL)
      : WorkerMainThreadRunnable(aWorkerPrivate, "EventSource :: Init"_ns),
        mESImpl(std::move(aEventSourceImpl)),
        mURL(aURL),
        mRv(NS_ERROR_NOT_INITIALIZED) {
    MOZ_ASSERT(aWorkerPrivate);
    aWorkerPrivate->AssertIsOnWorkerThread();
    MOZ_ASSERT(mESImpl);
  }

  bool MainThreadRun() override {
    // Get principal from worker's owner document or from worker.
    WorkerPrivate* wp = mWorkerPrivate;
    while (wp->GetParent()) {
      wp = wp->GetParent();
    }
    nsPIDOMWindowInner* window = wp->GetWindow();
    Document* doc = window ? window->GetExtantDoc() : nullptr;
    nsCOMPtr<nsIPrincipal> principal =
        doc ? doc->NodePrincipal() : wp->GetPrincipal();
    if (!principal) {
      mRv = NS_ERROR_FAILURE;
      return true;
    }
    ErrorResult rv;
    mESImpl->Init(principal, mURL, rv);
    mRv = rv.StealNSResult();

    // We want to ensure that EventSourceImpl's lifecycle
    // does not depend on this Runnable's one.
    mESImpl = nullptr;

    return true;
  }

  nsresult ErrorCode() const { return mRv; }

 private:
  RefPtr<EventSourceImpl> mESImpl;
  const nsAString& mURL;
  nsresult mRv;
};

class ConnectRunnable final : public WorkerMainThreadRunnable {
 public:
  explicit ConnectRunnable(WorkerPrivate* aWorkerPrivate,
                           RefPtr<EventSourceImpl> aEventSourceImpl)
      : WorkerMainThreadRunnable(aWorkerPrivate, "EventSource :: Connect"_ns),
        mESImpl(std::move(aEventSourceImpl)) {
    MOZ_ASSERT(aWorkerPrivate);
    aWorkerPrivate->AssertIsOnWorkerThread();
    MOZ_ASSERT(mESImpl);
  }

  bool MainThreadRun() override {
    MOZ_ASSERT(mESImpl);
    // We are allowed to access the event target since this runnable is
    // synchronized with the thread the event target lives on.
    mESImpl->InitChannelAndRequestEventSource(true);
    // We want to ensure the shortest possible remaining lifetime
    // and not depend on the Runnable's destruction.
    mESImpl = nullptr;
    return true;
  }

 private:
  RefPtr<EventSourceImpl> mESImpl;
};

nsresult EventSourceImpl::ParseURL(const nsAString& aURL) {
  AssertIsOnMainThread();
  MOZ_ASSERT(!mIsShutDown);
  // get the src
  nsCOMPtr<nsIURI> baseURI;
  nsresult rv = GetBaseURI(getter_AddRefs(baseURI));
  NS_ENSURE_SUCCESS(rv, rv);

  nsCOMPtr<nsIURI> srcURI;
  rv = NS_NewURI(getter_AddRefs(srcURI), aURL, nullptr, baseURI);
  NS_ENSURE_SUCCESS(rv, NS_ERROR_DOM_SYNTAX_ERR);

  nsAutoString origin;
  rv = nsContentUtils::GetWebExposedOriginSerialization(srcURI, origin);
  NS_ENSURE_SUCCESS(rv, rv);

  nsAutoCString spec;
  rv = srcURI->GetSpec(spec);
  NS_ENSURE_SUCCESS(rv, rv);

  // This assignment doesn't require extra synchronization because this function
  // is only ever called from EventSourceImpl::Init(), which is either called
  // directly if mEventSource was created on the main thread, or via a
  // synchronous runnable if it was created on a worker thread.
  {
    // We can't use GetEventSource() here because it would modify the refcount,
    // and that's not allowed off the owning thread.
    auto lock = mSharedData.Lock();
    lock->mEventSource->mOriginalURL = NS_ConvertUTF8toUTF16(spec);
  }
  mSrc = srcURI;
  mOrigin = origin;
  return NS_OK;
}

nsresult EventSourceImpl::AddWindowObservers() {
  AssertIsOnMainThread();
  MOZ_ASSERT(mIsMainThread);
  MOZ_ASSERT(!mIsShutDown);
  nsCOMPtr<nsIObserverService> os = mozilla::services::GetObserverService();
  NS_ENSURE_STATE(os);

  nsresult rv = os->AddObserver(this, DOM_WINDOW_DESTROYED_TOPIC, true);
  NS_ENSURE_SUCCESS(rv, rv);
  rv = os->AddObserver(this, DOM_WINDOW_FROZEN_TOPIC, true);
  NS_ENSURE_SUCCESS(rv, rv);
  rv = os->AddObserver(this, DOM_WINDOW_THAWED_TOPIC, true);
  NS_ENSURE_SUCCESS(rv, rv);
  return NS_OK;
}

void EventSourceImpl::RemoveWindowObservers() {
  AssertIsOnMainThread();
  MOZ_ASSERT(mIsMainThread);
  MOZ_ASSERT(IsClosed());
  nsCOMPtr<nsIObserverService> os = mozilla::services::GetObserverService();
  if (os) {
    os->RemoveObserver(this, DOM_WINDOW_DESTROYED_TOPIC);
    os->RemoveObserver(this, DOM_WINDOW_FROZEN_TOPIC);
    os->RemoveObserver(this, DOM_WINDOW_THAWED_TOPIC);
  }
}

void EventSourceImpl::Init(nsIPrincipal* aPrincipal, const nsAString& aURL,
                           ErrorResult& aRv) {
  AssertIsOnMainThread();
  MOZ_ASSERT(aPrincipal);
  MOZ_ASSERT(ReadyState() == CONNECTING);
  mPrincipal = aPrincipal;
  aRv = ParseURL(aURL);
  if (NS_WARN_IF(aRv.Failed())) {
    return;
  }
  // The conditional here is historical and not necessarily sane.
  if (JSContext* cx = nsContentUtils::GetCurrentJSContext()) {
    nsJSUtils::GetCallingLocation(cx, mScriptFile, &mScriptLine,
                                  &mScriptColumn);
    mInnerWindowID = nsJSUtils::GetCurrentlyRunningCodeInnerWindowID(cx);
  }

  if (mIsMainThread) {
    // we observe when the window freezes and thaws
    aRv = AddWindowObservers();
    if (NS_WARN_IF(aRv.Failed())) {
      return;
    }
  }

  mReconnectionTime =
      Preferences::GetInt("dom.server-events.default-reconnection-time",
                          DEFAULT_RECONNECTION_TIME_VALUE);

  mUnicodeDecoder = UTF_8_ENCODING->NewDecoderWithBOMRemoval();
}

//-----------------------------------------------------------------------------
// EventSourceImpl::nsIObserver
//-----------------------------------------------------------------------------

NS_IMETHODIMP
EventSourceImpl::Observe(nsISupports* aSubject, const char* aTopic,
                         const char16_t* aData) {
  AssertIsOnMainThread();
  if (IsClosed()) {
    return NS_OK;
  }

  nsCOMPtr<nsPIDOMWindowInner> window = do_QueryInterface(aSubject);
  MOZ_ASSERT(mIsMainThread);
  {
    auto lock = mSharedData.Lock();
    if (!lock->mEventSource->GetOwner() ||
        window != lock->mEventSource->GetOwner()) {
      return NS_OK;
    }
  }

  DebugOnly<nsresult> rv;
  if (strcmp(aTopic, DOM_WINDOW_FROZEN_TOPIC) == 0) {
    rv = Freeze();
    MOZ_ASSERT(NS_SUCCEEDED(rv), "Freeze() failed");
  } else if (strcmp(aTopic, DOM_WINDOW_THAWED_TOPIC) == 0) {
    rv = Thaw();
    MOZ_ASSERT(NS_SUCCEEDED(rv), "Thaw() failed");
  } else if (strcmp(aTopic, DOM_WINDOW_DESTROYED_TOPIC) == 0) {
    Close();
  }

  return NS_OK;
}

//-----------------------------------------------------------------------------
// EventSourceImpl::nsIStreamListener
//-----------------------------------------------------------------------------

NS_IMETHODIMP
EventSourceImpl::OnStartRequest(nsIRequest* aRequest) {
  AssertIsOnMainThread();
  if (IsClosed()) {
    return NS_ERROR_ABORT;
  }
  nsresult rv = CheckHealthOfRequestCallback(aRequest);
  NS_ENSURE_SUCCESS(rv, rv);

  nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(aRequest, &rv);
  NS_ENSURE_SUCCESS(rv, rv);

  nsresult status;
  rv = aRequest->GetStatus(&status);
  NS_ENSURE_SUCCESS(rv, rv);

  if (NS_FAILED(status)) {
    // EventSource::OnStopRequest will evaluate if it shall either reestablish
    // or fail the connection
    return NS_ERROR_ABORT;
  }

  uint32_t httpStatus;
  rv = httpChannel->GetResponseStatus(&httpStatus);
  NS_ENSURE_SUCCESS(rv, rv);

  if (httpStatus != 200) {
    DispatchFailConnection();
    return NS_ERROR_ABORT;
  }

  nsAutoCString contentType;
  rv = httpChannel->GetContentType(contentType);
  NS_ENSURE_SUCCESS(rv, rv);

  if (!contentType.EqualsLiteral(TEXT_EVENT_STREAM)) {
    DispatchFailConnection();
    return NS_ERROR_ABORT;
  }

  if (!mIsMainThread) {
    // Try to retarget to worker thread, otherwise fall back to main thread.
    nsCOMPtr<nsIThreadRetargetableRequest> rr = do_QueryInterface(httpChannel);
    if (rr) {
      rv = rr->RetargetDeliveryTo(this);
      if (NS_WARN_IF(NS_FAILED(rv))) {
        NS_WARNING("Retargeting failed");
      }
    }
  }

  {
    auto lock = mSharedData.Lock();
    lock->mServiceNotifier = MakeUnique<EventSourceServiceNotifier>(
        this, mHttpChannel->ChannelId(), mInnerWindowID);
  }
  rv = Dispatch(NewRunnableMethod("dom::EventSourceImpl::AnnounceConnection",
                                  this, &EventSourceImpl::AnnounceConnection),
                NS_DISPATCH_NORMAL);
  NS_ENSURE_SUCCESS(rv, rv);
  mStatus = PARSE_STATE_BEGIN_OF_STREAM;
  return NS_OK;
}

// this method parses the characters as they become available instead of
// buffering them.
nsresult EventSourceImpl::StreamReaderFunc(nsIInputStream* aInputStream,
                                           void* aClosure,
                                           const char* aFromRawSegment,
                                           uint32_t aToOffset, uint32_t aCount,
                                           uint32_t* aWriteCount) {
  // The EventSourceImpl instance is hold alive on the
  // synchronously calling stack, so raw pointer is fine here.
  EventSourceImpl* thisObject = static_cast<EventSourceImpl*>(aClosure);
  if (!thisObject || !aWriteCount) {
    NS_WARNING(
        "EventSource cannot read from stream: no aClosure or aWriteCount");
    return NS_ERROR_FAILURE;
  }
  thisObject->AssertIsOnTargetThread();
  MOZ_ASSERT(!thisObject->mIsShutDown);
  thisObject->ParseSegment((const char*)aFromRawSegment, aCount);
  *aWriteCount = aCount;
  return NS_OK;
}

void EventSourceImpl::ParseSegment(const char* aBuffer, uint32_t aLength) {
  AssertIsOnTargetThread();
  if (IsClosed()) {
    return;
  }
  char16_t buffer[1024];
  auto dst = Span(buffer);
  auto src = AsBytes(Span(aBuffer, aLength));
  // XXX EOF handling is https://bugzilla.mozilla.org/show_bug.cgi?id=1369018
  for (;;) {
    uint32_t result;
    size_t read;
    size_t written;
    std::tie(result, read, written, std::ignore) =
        mUnicodeDecoder->DecodeToUTF16(src, dst, false);
    for (auto c : dst.To(written)) {
      nsresult rv = ParseCharacter(c);
      NS_ENSURE_SUCCESS_VOID(rv);
    }
    if (result == kInputEmpty) {
      return;
    }
    src = src.From(read);
  }
}

NS_IMETHODIMP
EventSourceImpl::OnDataAvailable(nsIRequest* aRequest,
                                 nsIInputStream* aInputStream, uint64_t aOffset,
                                 uint32_t aCount) {
  AssertIsOnTargetThread();
  NS_ENSURE_ARG_POINTER(aInputStream);
  if (IsClosed()) {
    return NS_ERROR_ABORT;
  }

  nsresult rv = CheckHealthOfRequestCallback(aRequest);
  NS_ENSURE_SUCCESS(rv, rv);

  uint32_t totalRead;
  return aInputStream->ReadSegments(EventSourceImpl::StreamReaderFunc, this,
                                    aCount, &totalRead);
}

NS_IMETHODIMP
EventSourceImpl::OnStopRequest(nsIRequest* aRequest, nsresult aStatusCode) {
  AssertIsOnMainThread();

  if (IsClosed()) {
    return NS_ERROR_ABORT;
  }
  MOZ_ASSERT(mSrc);
  // "Network errors that prevents the connection from being established in the
  //  first place (e.g. DNS errors), must cause the user agent to asynchronously
  //  reestablish the connection.
  //
  //  (...) the cancelation of the fetch algorithm by the user agent (e.g. in
  //  response to window.stop() or the user canceling the network connection
  //  manually) must cause the user agent to fail the connection.
  // There could be additional network errors that are not covered in the above
  // checks
  //  See Bug 1808511
  if (NS_FAILED(aStatusCode) && aStatusCode != NS_ERROR_CONNECTION_REFUSED &&
      aStatusCode != NS_ERROR_NET_TIMEOUT &&
      aStatusCode != NS_ERROR_NET_RESET &&
      aStatusCode != NS_ERROR_NET_INTERRUPT &&
      aStatusCode != NS_ERROR_NET_PARTIAL_TRANSFER &&
      aStatusCode != NS_ERROR_NET_TIMEOUT_EXTERNAL &&
      aStatusCode != NS_ERROR_PROXY_CONNECTION_REFUSED &&
      aStatusCode != NS_ERROR_DNS_LOOKUP_QUEUE_FULL) {
    DispatchFailConnection();
    return NS_ERROR_ABORT;
  }

  nsresult rv = CheckHealthOfRequestCallback(aRequest);
  NS_ENSURE_SUCCESS(rv, rv);

  rv =
      Dispatch(NewRunnableMethod("dom::EventSourceImpl::ReestablishConnection",
                                 this, &EventSourceImpl::ReestablishConnection),
               NS_DISPATCH_NORMAL);
  NS_ENSURE_SUCCESS(rv, rv);

  return NS_OK;
}

//-----------------------------------------------------------------------------
// EventSourceImpl::nsIChannelEventSink
//-----------------------------------------------------------------------------

NS_IMETHODIMP
EventSourceImpl::AsyncOnChannelRedirect(
    nsIChannel* aOldChannel, nsIChannel* aNewChannel, uint32_t aFlags,
    nsIAsyncVerifyRedirectCallback* aCallback) {
  AssertIsOnMainThread();
  if (IsClosed()) {
    return NS_ERROR_ABORT;
  }
  nsCOMPtr<nsIRequest> aOldRequest = aOldChannel;
  MOZ_ASSERT(aOldRequest, "Redirect from a null request?");

  nsresult rv = CheckHealthOfRequestCallback(aOldRequest);
  NS_ENSURE_SUCCESS(rv, rv);

  MOZ_ASSERT(aNewChannel, "Redirect without a channel?");

  nsCOMPtr<nsIURI> newURI;
  rv = NS_GetFinalChannelURI(aNewChannel, getter_AddRefs(newURI));
  NS_ENSURE_SUCCESS(rv, rv);

  bool isValidScheme = newURI->SchemeIs("http") || newURI->SchemeIs("https");

  rv =
      mIsMainThread ? GetEventSource()->CheckCurrentGlobalCorrectness() : NS_OK;
  if (NS_FAILED(rv) || !isValidScheme) {
    DispatchFailConnection();
    return NS_ERROR_DOM_SECURITY_ERR;
  }

  // update our channel

  mHttpChannel = do_QueryInterface(aNewChannel);
  NS_ENSURE_STATE(mHttpChannel);

  SetupHttpChannel();
  // The HTTP impl already copies over the referrer info on
  // redirects, so we don't need to SetupReferrerInfo().

  if ((aFlags & nsIChannelEventSink::REDIRECT_PERMANENT) != 0) {
    rv = NS_GetFinalChannelURI(mHttpChannel, getter_AddRefs(mSrc));
    NS_ENSURE_SUCCESS(rv, rv);
  }

  aCallback->OnRedirectVerifyCallback(NS_OK);

  return NS_OK;
}

//-----------------------------------------------------------------------------
// EventSourceImpl::nsIInterfaceRequestor
//-----------------------------------------------------------------------------

NS_IMETHODIMP
EventSourceImpl::GetInterface(const nsIID& aIID, void** aResult) {
  AssertIsOnMainThread();

  if (IsClosed()) {
    return NS_ERROR_FAILURE;
  }

  if (aIID.Equals(NS_GET_IID(nsIChannelEventSink))) {
    *aResult = static_cast<nsIChannelEventSink*>(this);
    NS_ADDREF_THIS();
    return NS_OK;
  }

  if (aIID.Equals(NS_GET_IID(nsIAuthPrompt)) ||
      aIID.Equals(NS_GET_IID(nsIAuthPrompt2))) {
    nsresult rv;
    nsCOMPtr<nsIPromptFactory> wwatch =
        do_GetService(NS_WINDOWWATCHER_CONTRACTID, &rv);
    NS_ENSURE_SUCCESS(rv, rv);

    nsCOMPtr<nsPIDOMWindowOuter> window;

    // To avoid a data race we may only access the event target if it lives on
    // the main thread.
    if (mIsMainThread) {
      auto lock = mSharedData.Lock();
      rv = lock->mEventSource->CheckCurrentGlobalCorrectness();
      NS_ENSURE_SUCCESS(rv, NS_ERROR_UNEXPECTED);

      if (lock->mEventSource->GetOwner()) {
        window = lock->mEventSource->GetOwner()->GetOuterWindow();
      }
    }

    // Get the an auth prompter for our window so that the parenting
    // of the dialogs works as it should when using tabs.

    return wwatch->GetPrompt(window, aIID, aResult);
  }

  return QueryInterface(aIID, aResult);
}

NS_IMETHODIMP
EventSourceImpl::IsOnCurrentThread(bool* aResult) {
  *aResult = IsTargetThread();
  return NS_OK;
}

NS_IMETHODIMP_(bool)
EventSourceImpl::IsOnCurrentThreadInfallible() { return IsTargetThread(); }

nsresult EventSourceImpl::GetBaseURI(nsIURI** aBaseURI) {
  AssertIsOnMainThread();
  MOZ_ASSERT(!mIsShutDown);
  NS_ENSURE_ARG_POINTER(aBaseURI);

  *aBaseURI = nullptr;

  nsCOMPtr<nsIURI> baseURI;

  // first we try from document->GetBaseURI()
  nsCOMPtr<Document> doc =
      mIsMainThread ? GetEventSource()->GetDocumentIfCurrent() : nullptr;
  if (doc) {
    baseURI = doc->GetBaseURI();
  }

  // otherwise we get from the doc's principal
  if (!baseURI) {
    auto* basePrin = BasePrincipal::Cast(mPrincipal);
    nsresult rv = basePrin->GetURI(getter_AddRefs(baseURI));
    NS_ENSURE_SUCCESS(rv, rv);
  }

  NS_ENSURE_STATE(baseURI);

  baseURI.forget(aBaseURI);
  return NS_OK;
}

void EventSourceImpl::SetupHttpChannel() {
  AssertIsOnMainThread();
  MOZ_ASSERT(!mIsShutDown);
  nsresult rv = mHttpChannel->SetRequestMethod("GET"_ns);
  MOZ_ASSERT(NS_SUCCEEDED(rv));

  /* set the http request headers */

  rv = mHttpChannel->SetRequestHeader(
      "Accept"_ns, nsLiteralCString(TEXT_EVENT_STREAM), false);
  MOZ_ASSERT(NS_SUCCEEDED(rv));

  // LOAD_BYPASS_CACHE already adds the Cache-Control: no-cache header

  if (mLastEventID.IsEmpty()) {
    return;
  }
  NS_ConvertUTF16toUTF8 eventId(mLastEventID);
  rv = mHttpChannel->SetRequestHeader("Last-Event-ID"_ns, eventId, false);
#ifdef DEBUG
  if (NS_FAILED(rv)) {
    MOZ_LOG(gEventSourceLog, LogLevel::Warning,
            ("SetupHttpChannel. rv=%x (%s)", uint32_t(rv), eventId.get()));
  }
#endif
  Unused << rv;
}

nsresult EventSourceImpl::SetupReferrerInfo(
    const nsCOMPtr<Document>& aDocument) {
  AssertIsOnMainThread();
  MOZ_ASSERT(!mIsShutDown);

  if (aDocument) {
    auto referrerInfo = MakeRefPtr<ReferrerInfo>(*aDocument);
    nsresult rv = mHttpChannel->SetReferrerInfoWithoutClone(referrerInfo);
    NS_ENSURE_SUCCESS(rv, rv);
  }

  return NS_OK;
}

nsresult EventSourceImpl::InitChannelAndRequestEventSource(
    const bool aEventTargetAccessAllowed) {
  AssertIsOnMainThread();
  if (IsClosed()) {
    return NS_ERROR_ABORT;
  }

  bool isValidScheme = mSrc->SchemeIs("http") || mSrc->SchemeIs("https");

  MOZ_ASSERT_IF(mIsMainThread, aEventTargetAccessAllowed);

  nsresult rv = aEventTargetAccessAllowed ? [this]() {
    // We can't call GetEventSource() because we're not
    // allowed to touch the refcount off the worker thread
    // due to an assertion, event if it would have otherwise
    // been safe.
    auto lock = mSharedData.Lock();
    return lock->mEventSource->CheckCurrentGlobalCorrectness();
  }()
                                          : NS_OK;
  if (NS_FAILED(rv) || !isValidScheme) {
    DispatchFailConnection();
    return NS_ERROR_DOM_SECURITY_ERR;
  }

  nsCOMPtr<Document> doc;
  nsSecurityFlags securityFlags =
      nsILoadInfo::SEC_REQUIRE_CORS_INHERITS_SEC_CONTEXT;
  {
    auto lock = mSharedData.Lock();
    doc = aEventTargetAccessAllowed ? lock->mEventSource->GetDocumentIfCurrent()
                                    : nullptr;

    if (lock->mEventSource->mWithCredentials) {
      securityFlags |= nsILoadInfo::SEC_COOKIES_INCLUDE;
    }
  }

  // The html spec requires we use fetch cache mode of "no-store".  This
  // maps to LOAD_BYPASS_CACHE and LOAD_INHIBIT_CACHING in necko.
  nsLoadFlags loadFlags;
  loadFlags = nsIRequest::LOAD_BACKGROUND | nsIRequest::LOAD_BYPASS_CACHE |
              nsIRequest::INHIBIT_CACHING;

  nsCOMPtr<nsIChannel> channel;
  // If we have the document, use it
  if (doc) {
    MOZ_ASSERT(mCookieJarSettings == doc->CookieJarSettings());

    nsCOMPtr<nsILoadGroup> loadGroup = doc->GetDocumentLoadGroup();
    rv = NS_NewChannel(getter_AddRefs(channel), mSrc, doc, securityFlags,
                       nsIContentPolicy::TYPE_INTERNAL_EVENTSOURCE,
                       nullptr,  // aPerformanceStorage
                       loadGroup,
                       nullptr,     // aCallbacks
                       loadFlags);  // aLoadFlags
  } else {
    // otherwise use the principal
    rv = NS_NewChannel(getter_AddRefs(channel), mSrc, mPrincipal, securityFlags,
                       nsIContentPolicy::TYPE_INTERNAL_EVENTSOURCE,
                       mCookieJarSettings,
                       nullptr,     // aPerformanceStorage
                       nullptr,     // loadGroup
                       nullptr,     // aCallbacks
                       loadFlags);  // aLoadFlags
  }

  NS_ENSURE_SUCCESS(rv, rv);

  mHttpChannel = do_QueryInterface(channel);
  NS_ENSURE_TRUE(mHttpChannel, NS_ERROR_NO_INTERFACE);

  SetupHttpChannel();
  rv = SetupReferrerInfo(doc);
  NS_ENSURE_SUCCESS(rv, rv);

#ifdef DEBUG
  {
    nsCOMPtr<nsIInterfaceRequestor> notificationCallbacks;
    mHttpChannel->GetNotificationCallbacks(
        getter_AddRefs(notificationCallbacks));
    MOZ_ASSERT(!notificationCallbacks);
  }
#endif

  mHttpChannel->SetNotificationCallbacks(this);

  // Start reading from the channel
  rv = mHttpChannel->AsyncOpen(this);
  if (NS_FAILED(rv)) {
    DispatchFailConnection();
    return rv;
  }

  return rv;
}

void EventSourceImpl::AnnounceConnection() {
  AssertIsOnTargetThread();
  if (ReadyState() != CONNECTING) {
    NS_WARNING("Unexpected mReadyState!!!");
    return;
  }

  {
    auto lock = mSharedData.Lock();
    if (lock->mServiceNotifier) {
      lock->mServiceNotifier->ConnectionOpened();
    }
  }

  // When a user agent is to announce the connection, the user agent must set
  // the readyState attribute to OPEN and queue a task to fire a simple event
  // named open at the EventSource object.

  SetReadyState(OPEN);

  nsresult rv = GetEventSource()->CheckCurrentGlobalCorrectness();
  if (NS_FAILED(rv)) {
    return;
  }
  // We can't hold the mutex while dispatching the event because the mutex is
  // not reentrant, and content might call back into our code.
  rv = GetEventSource()->CreateAndDispatchSimpleEvent(u"open"_ns);
  if (NS_FAILED(rv)) {
    NS_WARNING("Failed to dispatch the error event!!!");
    return;
  }
}

nsresult EventSourceImpl::ResetConnection() {
  AssertIsOnMainThread();
  if (mHttpChannel) {
    mHttpChannel->Cancel(NS_ERROR_ABORT);
    mHttpChannel = nullptr;
  }
  return NS_OK;
}

void EventSourceImpl::ResetDecoder() {
  AssertIsOnTargetThread();
  if (mUnicodeDecoder) {
    UTF_8_ENCODING->NewDecoderWithBOMRemovalInto(*mUnicodeDecoder);
  }
  mStatus = PARSE_STATE_OFF;
  ClearFields();
}

class CallRestartConnection final : public WorkerMainThreadRunnable {
 public:
  explicit CallRestartConnection(RefPtr<EventSourceImpl>&& aEventSourceImpl)
      : WorkerMainThreadRunnable(aEventSourceImpl->mWorkerRef->Private(),
                                 "EventSource :: RestartConnection"_ns),
        mESImpl(std::move(aEventSourceImpl)) {
    mWorkerPrivate->AssertIsOnWorkerThread();
    MOZ_ASSERT(mESImpl);
  }

  bool MainThreadRun() override {
    MOZ_ASSERT(mESImpl);
    mESImpl->RestartConnection();
    // We want to ensure the shortest possible remaining lifetime
    // and not depend on the Runnable's destruction.
    mESImpl = nullptr;
    return true;
  }

 protected:
  RefPtr<EventSourceImpl> mESImpl;
};

nsresult EventSourceImpl::RestartConnection() {
  AssertIsOnMainThread();
  if (IsClosed()) {
    return NS_ERROR_ABORT;
  }

  nsresult rv = ResetConnection();
  NS_ENSURE_SUCCESS(rv, rv);
  rv = SetReconnectionTimeout();
  NS_ENSURE_SUCCESS(rv, rv);
  return NS_OK;
}

void EventSourceImpl::ReestablishConnection() {
  AssertIsOnTargetThread();
  if (IsClosed()) {
    return;
  }

  nsresult rv;
  if (mIsMainThread) {
    rv = RestartConnection();
  } else {
    RefPtr<CallRestartConnection> runnable = new CallRestartConnection(this);
    ErrorResult result;
    runnable->Dispatch(Canceling, result);
    MOZ_ASSERT(!result.Failed());
    rv = result.StealNSResult();
  }
  if (NS_FAILED(rv)) {
    return;
  }

  rv = GetEventSource()->CheckCurrentGlobalCorrectness();
  if (NS_FAILED(rv)) {
    return;
  }

  SetReadyState(CONNECTING);
  ResetDecoder();
  // We can't hold the mutex while dispatching the event because the mutex is
  // not reentrant, and content might call back into our code.
  rv = GetEventSource()->CreateAndDispatchSimpleEvent(u"error"_ns);
  if (NS_FAILED(rv)) {
    NS_WARNING("Failed to dispatch the error event!!!");
    return;
  }
}

nsresult EventSourceImpl::SetReconnectionTimeout() {
  AssertIsOnMainThread();
  if (IsClosed()) {
    return NS_ERROR_ABORT;
  }

  // the timer will be used whenever the requests are going finished.
  if (!mTimer) {
    mTimer = NS_NewTimer();
    NS_ENSURE_STATE(mTimer);
  }

  MOZ_TRY(mTimer->InitWithCallback(this, mReconnectionTime,
                                   nsITimer::TYPE_ONE_SHOT));

  return NS_OK;
}

nsresult EventSourceImpl::PrintErrorOnConsole(
    const char* aBundleURI, const char* aError,
    const nsTArray<nsString>& aFormatStrings) {
  AssertIsOnMainThread();
  MOZ_ASSERT(!mIsShutDown);
  nsCOMPtr<nsIStringBundleService> bundleService =
      mozilla::components::StringBundle::Service();
  NS_ENSURE_STATE(bundleService);

  nsCOMPtr<nsIStringBundle> strBundle;
  nsresult rv =
      bundleService->CreateBundle(aBundleURI, getter_AddRefs(strBundle));
  NS_ENSURE_SUCCESS(rv, rv);

  nsCOMPtr<nsIConsoleService> console(
      do_GetService(NS_CONSOLESERVICE_CONTRACTID, &rv));
  NS_ENSURE_SUCCESS(rv, rv);

  nsCOMPtr<nsIScriptError> errObj(
      do_CreateInstance(NS_SCRIPTERROR_CONTRACTID, &rv));
  NS_ENSURE_SUCCESS(rv, rv);

  // Localize the error message
  nsAutoString message;
  if (!aFormatStrings.IsEmpty()) {
    rv = strBundle->FormatStringFromName(aError, aFormatStrings, message);
  } else {
    rv = strBundle->GetStringFromName(aError, message);
  }
  NS_ENSURE_SUCCESS(rv, rv);

  rv = errObj->InitWithWindowID(message, mScriptFile, u""_ns, mScriptLine,
                                mScriptColumn, nsIScriptError::errorFlag,
                                "Event Source", mInnerWindowID);
  NS_ENSURE_SUCCESS(rv, rv);

  // print the error message directly to the JS console
  rv = console->LogMessage(errObj);
  NS_ENSURE_SUCCESS(rv, rv);

  return NS_OK;
}

nsresult EventSourceImpl::ConsoleError() {
  AssertIsOnMainThread();
  MOZ_ASSERT(!mIsShutDown);
  nsAutoCString targetSpec;
  nsresult rv = mSrc->GetSpec(targetSpec);
  NS_ENSURE_SUCCESS(rv, rv);

  AutoTArray<nsString, 1> formatStrings;
  CopyUTF8toUTF16(targetSpec, *formatStrings.AppendElement());

  if (ReadyState() == CONNECTING) {
    rv = PrintErrorOnConsole("chrome://global/locale/appstrings.properties",
                             "connectionFailure", formatStrings);
  } else {
    rv = PrintErrorOnConsole("chrome://global/locale/appstrings.properties",
                             "netInterrupt", formatStrings);
  }
  NS_ENSURE_SUCCESS(rv, rv);

  return NS_OK;
}

void EventSourceImpl::DispatchFailConnection() {
  AssertIsOnMainThread();
  if (IsClosed()) {
    return;
  }
  nsresult rv = ConsoleError();
  if (NS_FAILED(rv)) {
    NS_WARNING("Failed to print to the console error");
  }
  rv = Dispatch(NewRunnableMethod("dom::EventSourceImpl::FailConnection", this,
                                  &EventSourceImpl::FailConnection),
                NS_DISPATCH_NORMAL);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    // if the worker is shutting down, the dispatching of normal WorkerRunnables
    // fails.
    return;
  }
}

void EventSourceImpl::FailConnection() {
  AssertIsOnTargetThread();
  if (IsClosed()) {
    return;
  }
  // Must change state to closed before firing event to content.
  SetReadyState(CLOSED);
  // When a user agent is to fail the connection, the user agent must set the
  // readyState attribute to CLOSED and queue a task to fire a simple event
  // named error at the EventSource object.
  nsresult rv = GetEventSource()->CheckCurrentGlobalCorrectness();
  if (NS_SUCCEEDED(rv)) {
    // We can't hold the mutex while dispatching the event because the mutex
    // is not reentrant, and content might call back into our code.
    rv = GetEventSource()->CreateAndDispatchSimpleEvent(u"error"_ns);
    if (NS_FAILED(rv)) {
      NS_WARNING("Failed to dispatch the error event!!!");
    }
  }
  // Call CloseInternal in the end of function because it may release
  // EventSourceImpl.
  CloseInternal();
}

NS_IMETHODIMP EventSourceImpl::Notify(nsITimer* aTimer) {
  AssertIsOnMainThread();
  if (IsClosed()) {
    return NS_OK;
  }

  MOZ_ASSERT(!mHttpChannel, "the channel hasn't been cancelled!!");

  if (!mFrozen) {
    nsresult rv = InitChannelAndRequestEventSource(mIsMainThread);
    if (NS_FAILED(rv)) {
      NS_WARNING("InitChannelAndRequestEventSource() failed");
    }
  }
  return NS_OK;
}

NS_IMETHODIMP EventSourceImpl::GetName(nsACString& aName) {
  aName.AssignLiteral("EventSourceImpl");
  return NS_OK;
}

nsresult EventSourceImpl::Thaw() {
  AssertIsOnMainThread();
  if (IsClosed() || !mFrozen) {
    return NS_OK;
  }

  MOZ_ASSERT(!mHttpChannel, "the connection hasn't been closed!!!");

  mFrozen = false;
  nsresult rv;
  if (!mGoingToDispatchAllMessages && mMessagesToDispatch.GetSize() > 0) {
    nsCOMPtr<nsIRunnable> event =
        NewRunnableMethod("dom::EventSourceImpl::DispatchAllMessageEvents",
                          this, &EventSourceImpl::DispatchAllMessageEvents);
    NS_ENSURE_STATE(event);

    mGoingToDispatchAllMessages = true;

    rv = Dispatch(event.forget(), NS_DISPATCH_NORMAL);
    NS_ENSURE_SUCCESS(rv, rv);
  }

  rv = InitChannelAndRequestEventSource(mIsMainThread);
  NS_ENSURE_SUCCESS(rv, rv);

  return NS_OK;
}

nsresult EventSourceImpl::Freeze() {
  AssertIsOnMainThread();
  if (IsClosed() || mFrozen) {
    return NS_OK;
  }

  MOZ_ASSERT(!mHttpChannel, "the connection hasn't been closed!!!");
  mFrozen = true;
  return NS_OK;
}

nsresult EventSourceImpl::DispatchCurrentMessageEvent() {
  AssertIsOnTargetThread();
  MOZ_ASSERT(!mIsShutDown);
  UniquePtr<Message> message(std::move(mCurrentMessage));
  ClearFields();

  if (!message || message->mData.IsEmpty()) {
    return NS_OK;
  }

  // removes the trailing LF from mData
  MOZ_ASSERT(message->mData.CharAt(message->mData.Length() - 1) == LF_CHAR,
             "Invalid trailing character! LF was expected instead.");
  message->mData.SetLength(message->mData.Length() - 1);

  if (message->mEventName.IsEmpty()) {
    message->mEventName.AssignLiteral("message");
  }

  mMessagesToDispatch.Push(message.release());

  if (!mGoingToDispatchAllMessages) {
    nsCOMPtr<nsIRunnable> event =
        NewRunnableMethod("dom::EventSourceImpl::DispatchAllMessageEvents",
                          this, &EventSourceImpl::DispatchAllMessageEvents);
    NS_ENSURE_STATE(event);

    mGoingToDispatchAllMessages = true;

    return Dispatch(event.forget(), NS_DISPATCH_NORMAL);
  }

  return NS_OK;
}

void EventSourceImpl::DispatchAllMessageEvents() {
  AssertIsOnTargetThread();
  mGoingToDispatchAllMessages = false;

  if (IsClosed() || mFrozen) {
    return;
  }

  nsresult rv;
  AutoJSAPI jsapi;
  {
    auto lock = mSharedData.Lock();
    rv = lock->mEventSource->CheckCurrentGlobalCorrectness();
    if (NS_FAILED(rv)) {
      return;
    }

    if (NS_WARN_IF(!jsapi.Init(lock->mEventSource->GetOwnerGlobal()))) {
      return;
    }
  }

  JSContext* cx = jsapi.cx();

  while (mMessagesToDispatch.GetSize() > 0) {
    UniquePtr<Message> message(mMessagesToDispatch.PopFront());

    if (message->mLastEventID.isSome()) {
      mLastEventID.Assign(message->mLastEventID.value());
    }

    if (message->mLastEventID.isNothing() && !mLastEventID.IsEmpty()) {
      message->mLastEventID = Some(mLastEventID);
    }

    {
      auto lock = mSharedData.Lock();
      if (lock->mServiceNotifier) {
        lock->mServiceNotifier->EventReceived(message->mEventName, mLastEventID,
                                              message->mData, mReconnectionTime,
                                              PR_Now());
      }
    }

    // Now we can turn our string into a jsval
    JS::Rooted<JS::Value> jsData(cx);
    {
      JSString* jsString;
      jsString = JS_NewUCStringCopyN(cx, message->mData.get(),
                                     message->mData.Length());
      NS_ENSURE_TRUE_VOID(jsString);

      jsData.setString(jsString);
    }

    // create an event that uses the MessageEvent interface,
    // which does not bubble, is not cancelable, and has no default action

    RefPtr<EventSource> eventSource = GetEventSource();
    RefPtr<MessageEvent> event =
        new MessageEvent(eventSource, nullptr, nullptr);

    event->InitMessageEvent(nullptr, message->mEventName, CanBubble::eNo,
                            Cancelable::eNo, jsData, mOrigin, mLastEventID,
                            nullptr, Sequence<OwningNonNull<MessagePort>>());
    event->SetTrusted(true);

    // We can't hold the mutex while dispatching the event because the mutex is
    // not reentrant, and content might call back into our code.
    IgnoredErrorResult err;
    eventSource->DispatchEvent(*event, err);
    if (err.Failed()) {
      NS_WARNING("Failed to dispatch the message event!!!");
      return;
    }

    if (IsClosed() || mFrozen) {
      return;
    }
  }
}

void EventSourceImpl::ClearFields() {
  AssertIsOnTargetThread();
  mCurrentMessage = nullptr;
  mLastFieldName.Truncate();
  mLastFieldValue.Truncate();
}

nsresult EventSourceImpl::SetFieldAndClear() {
  MOZ_ASSERT(!mIsShutDown);
  AssertIsOnTargetThread();
  if (mLastFieldName.IsEmpty()) {
    mLastFieldValue.Truncate();
    return NS_OK;
  }
  if (!mCurrentMessage) {
    mCurrentMessage = MakeUnique<Message>();
  }
  char16_t first_char;
  first_char = mLastFieldName.CharAt(0);

  // with no case folding performed
  switch (first_char) {
    case char16_t('d'):
      if (mLastFieldName.EqualsLiteral("data")) {
        // If the field name is "data" append the field value to the data
        // buffer, then append a single U+000A LINE FEED (LF) character
        // to the data buffer.
        mCurrentMessage->mData.Append(mLastFieldValue);
        mCurrentMessage->mData.Append(LF_CHAR);
      }
      break;

    case char16_t('e'):
      if (mLastFieldName.EqualsLiteral("event")) {
        mCurrentMessage->mEventName.Assign(mLastFieldValue);
      }
      break;

    case char16_t('i'):
      if (mLastFieldName.EqualsLiteral("id")) {
        mCurrentMessage->mLastEventID = Some(mLastFieldValue);
      }
      break;

    case char16_t('r'):
      if (mLastFieldName.EqualsLiteral("retry")) {
        uint32_t newValue = 0;
        uint32_t i = 0;  // we must ensure that there are only digits
        bool assign = true;
        for (i = 0; i < mLastFieldValue.Length(); ++i) {
          if (mLastFieldValue.CharAt(i) < (char16_t)'0' ||
              mLastFieldValue.CharAt(i) > (char16_t)'9') {
            assign = false;
            break;
          }
          newValue = newValue * 10 + (((uint32_t)mLastFieldValue.CharAt(i)) -
                                      ((uint32_t)((char16_t)'0')));
        }

        if (assign) {
          if (newValue < MIN_RECONNECTION_TIME_VALUE) {
            mReconnectionTime = MIN_RECONNECTION_TIME_VALUE;
          } else if (newValue > MAX_RECONNECTION_TIME_VALUE) {
            mReconnectionTime = MAX_RECONNECTION_TIME_VALUE;
          } else {
            mReconnectionTime = newValue;
          }
        }
        break;
      }
      break;
  }

  mLastFieldName.Truncate();
  mLastFieldValue.Truncate();

  return NS_OK;
}

nsresult EventSourceImpl::CheckHealthOfRequestCallback(
    nsIRequest* aRequestCallback) {
  // This function could be run on target thread if http channel support
  // nsIThreadRetargetableRequest. otherwise, it's run on main thread.

  // check if we have been closed or if the request has been canceled
  // or if we have been frozen
  if (IsClosed() || mFrozen || !mHttpChannel) {
    return NS_ERROR_ABORT;
  }

  nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(aRequestCallback);
  NS_ENSURE_STATE(httpChannel);

  if (httpChannel != mHttpChannel) {
    NS_WARNING("wrong channel from request callback");
    return NS_ERROR_ABORT;
  }

  return NS_OK;
}

nsresult EventSourceImpl::ParseCharacter(char16_t aChr) {
  AssertIsOnTargetThread();
  nsresult rv;

  if (IsClosed()) {
    return NS_ERROR_ABORT;
  }

  switch (mStatus) {
    case PARSE_STATE_OFF:
      NS_ERROR("Invalid state");
      return NS_ERROR_FAILURE;
      break;

    case PARSE_STATE_BEGIN_OF_STREAM:
      if (aChr == CR_CHAR) {
        mStatus = PARSE_STATE_CR_CHAR;
      } else if (aChr == LF_CHAR) {
        mStatus = PARSE_STATE_BEGIN_OF_LINE;
      } else if (aChr == COLON_CHAR) {
        mStatus = PARSE_STATE_COMMENT;
      } else {
        mLastFieldName += aChr;
        mStatus = PARSE_STATE_FIELD_NAME;
      }
      break;

    case PARSE_STATE_CR_CHAR:
      if (aChr == CR_CHAR) {
        rv = DispatchCurrentMessageEvent();  // there is an empty line (CRCR)
        NS_ENSURE_SUCCESS(rv, rv);
      } else if (aChr == LF_CHAR) {
        mStatus = PARSE_STATE_BEGIN_OF_LINE;
      } else if (aChr == COLON_CHAR) {
        mStatus = PARSE_STATE_COMMENT;
      } else {
        mLastFieldName += aChr;
        mStatus = PARSE_STATE_FIELD_NAME;
      }

      break;

    case PARSE_STATE_COMMENT:
      if (aChr == CR_CHAR) {
        mStatus = PARSE_STATE_CR_CHAR;
      } else if (aChr == LF_CHAR) {
        mStatus = PARSE_STATE_BEGIN_OF_LINE;
      }

      break;

    case PARSE_STATE_FIELD_NAME:
      if (aChr == CR_CHAR) {
        rv = SetFieldAndClear();
        NS_ENSURE_SUCCESS(rv, rv);

        mStatus = PARSE_STATE_CR_CHAR;
      } else if (aChr == LF_CHAR) {
        rv = SetFieldAndClear();
        NS_ENSURE_SUCCESS(rv, rv);

        mStatus = PARSE_STATE_BEGIN_OF_LINE;
      } else if (aChr == COLON_CHAR) {
        mStatus = PARSE_STATE_FIRST_CHAR_OF_FIELD_VALUE;
      } else {
        mLastFieldName += aChr;
      }

      break;

    case PARSE_STATE_FIRST_CHAR_OF_FIELD_VALUE:
      if (aChr == CR_CHAR) {
        rv = SetFieldAndClear();
        NS_ENSURE_SUCCESS(rv, rv);

        mStatus = PARSE_STATE_CR_CHAR;
      } else if (aChr == LF_CHAR) {
        rv = SetFieldAndClear();
        NS_ENSURE_SUCCESS(rv, rv);

        mStatus = PARSE_STATE_BEGIN_OF_LINE;
      } else if (aChr == SPACE_CHAR) {
        mStatus = PARSE_STATE_FIELD_VALUE;
      } else {
        mLastFieldValue += aChr;
        mStatus = PARSE_STATE_FIELD_VALUE;
      }

      break;

    case PARSE_STATE_FIELD_VALUE:
      if (aChr == CR_CHAR) {
        rv = SetFieldAndClear();
        NS_ENSURE_SUCCESS(rv, rv);

        mStatus = PARSE_STATE_CR_CHAR;
      } else if (aChr == LF_CHAR) {
        rv = SetFieldAndClear();
        NS_ENSURE_SUCCESS(rv, rv);

        mStatus = PARSE_STATE_BEGIN_OF_LINE;
      } else if (aChr != 0) {
        // Avoid appending the null char to the field value.
        mLastFieldValue += aChr;
      } else if (mLastFieldName.EqualsLiteral("id")) {
        // Ignore the whole id field if aChr is null
        mStatus = PARSE_STATE_IGNORE_FIELD_VALUE;
        mLastFieldValue.Truncate();
      }

      break;

    case PARSE_STATE_IGNORE_FIELD_VALUE:
      if (aChr == CR_CHAR) {
        mStatus = PARSE_STATE_CR_CHAR;
      } else if (aChr == LF_CHAR) {
        mStatus = PARSE_STATE_BEGIN_OF_LINE;
      }
      break;

    case PARSE_STATE_BEGIN_OF_LINE:
      if (aChr == CR_CHAR) {
        rv = DispatchCurrentMessageEvent();  // there is an empty line
        NS_ENSURE_SUCCESS(rv, rv);

        mStatus = PARSE_STATE_CR_CHAR;
      } else if (aChr == LF_CHAR) {
        rv = DispatchCurrentMessageEvent();  // there is an empty line
        NS_ENSURE_SUCCESS(rv, rv);

        mStatus = PARSE_STATE_BEGIN_OF_LINE;
      } else if (aChr == COLON_CHAR) {
        mStatus = PARSE_STATE_COMMENT;
      } else if (aChr != 0) {
        // Avoid appending the null char to the field name.
        mLastFieldName += aChr;
        mStatus = PARSE_STATE_FIELD_NAME;
      }

      break;
  }

  return NS_OK;
}

namespace {

class WorkerRunnableDispatcher final : public WorkerRunnable {
  RefPtr<EventSourceImpl> mEventSourceImpl;

 public:
  WorkerRunnableDispatcher(RefPtr<EventSourceImpl>&& aImpl,
                           WorkerPrivate* aWorkerPrivate,
                           already_AddRefed<nsIRunnable> aEvent)
      : WorkerRunnable(aWorkerPrivate, WorkerThreadUnchangedBusyCount),
        mEventSourceImpl(std::move(aImpl)),
        mEvent(std::move(aEvent)) {}

  bool WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) override {
    aWorkerPrivate->AssertIsOnWorkerThread();
    return !NS_FAILED(mEvent->Run());
  }

  void PostRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate,
               bool aRunResult) override {
    // Ensure we drop the RefPtr on the worker thread
    // and to not keep us alive longer than needed.
    mEventSourceImpl = nullptr;
  }

  bool PreDispatch(WorkerPrivate* aWorkerPrivate) override {
    // We don't call WorkerRunnable::PreDispatch because it would assert the
    // wrong thing about which thread we're on.  We're on whichever thread the
    // channel implementation is running on (probably the main thread or
    // transport thread).
    return true;
  }

  void PostDispatch(WorkerPrivate* aWorkerPrivate,
                    bool aDispatchResult) override {
    // We don't call WorkerRunnable::PostDispatch because it would assert the
    // wrong thing about which thread we're on.  We're on whichever thread the
    // channel implementation is running on (probably the main thread or
    // transport thread).
  }

 private:
  nsCOMPtr<nsIRunnable> mEvent;
};

}  // namespace

bool EventSourceImpl::CreateWorkerRef(WorkerPrivate* aWorkerPrivate) {
  MOZ_ASSERT(!mWorkerRef);
  MOZ_ASSERT(aWorkerPrivate);
  aWorkerPrivate->AssertIsOnWorkerThread();

  if (mIsShutDown) {
    return false;
  }

  RefPtr<EventSourceImpl> self = this;
  RefPtr<StrongWorkerRef> workerRef = StrongWorkerRef::Create(
      aWorkerPrivate, "EventSource", [self]() { self->Close(); });

  if (NS_WARN_IF(!workerRef)) {
    return false;
  }

  mWorkerRef = new ThreadSafeWorkerRef(workerRef);
  return true;
}

void EventSourceImpl::ReleaseWorkerRef() {
  MOZ_ASSERT(IsClosed());
  MOZ_ASSERT(IsCurrentThreadRunningWorker());
  mWorkerRef = nullptr;
}

//-----------------------------------------------------------------------------
// EventSourceImpl::nsIEventTarget
//-----------------------------------------------------------------------------
NS_IMETHODIMP
EventSourceImpl::DispatchFromScript(nsIRunnable* aEvent, uint32_t aFlags) {
  nsCOMPtr<nsIRunnable> event(aEvent);
  return Dispatch(event.forget(), aFlags);
}

NS_IMETHODIMP
EventSourceImpl::Dispatch(already_AddRefed<nsIRunnable> aEvent,
                          uint32_t aFlags) {
  nsCOMPtr<nsIRunnable> event_ref(aEvent);
  if (mIsMainThread) {
    return NS_DispatchToMainThread(event_ref.forget());
  }

  if (mIsShutDown) {
    // We want to avoid clutter about errors in our shutdown logs,
    // so just report NS_OK (we have no explicit return value
    // for shutdown).
    return NS_OK;
  }

  // If the target is a worker, we have to use a custom WorkerRunnableDispatcher
  // runnable.
  RefPtr<WorkerRunnableDispatcher> event = new WorkerRunnableDispatcher(
      this, mWorkerRef->Private(), event_ref.forget());

  if (!event->Dispatch()) {
    return NS_ERROR_FAILURE;
  }
  return NS_OK;
}

NS_IMETHODIMP
EventSourceImpl::DelayedDispatch(already_AddRefed<nsIRunnable> aEvent,
                                 uint32_t aDelayMs) {
  return NS_ERROR_NOT_IMPLEMENTED;
}

NS_IMETHODIMP
EventSourceImpl::RegisterShutdownTask(nsITargetShutdownTask*) {
  return NS_ERROR_NOT_IMPLEMENTED;
}

NS_IMETHODIMP
EventSourceImpl::UnregisterShutdownTask(nsITargetShutdownTask*) {
  return NS_ERROR_NOT_IMPLEMENTED;
}

//-----------------------------------------------------------------------------
// EventSourceImpl::nsIThreadRetargetableStreamListener
//-----------------------------------------------------------------------------
NS_IMETHODIMP
EventSourceImpl::CheckListenerChain() {
  MOZ_ASSERT(NS_IsMainThread(), "Should be on the main thread!");
  return NS_OK;
}
////////////////////////////////////////////////////////////////////////////////
// EventSource
////////////////////////////////////////////////////////////////////////////////

EventSource::EventSource(nsIGlobalObject* aGlobal,
                         nsICookieJarSettings* aCookieJarSettings,
                         bool aWithCredentials)
    : DOMEventTargetHelper(aGlobal),
      mWithCredentials(aWithCredentials),
      mIsMainThread(NS_IsMainThread()) {
  MOZ_ASSERT(aGlobal);
  MOZ_ASSERT(aCookieJarSettings);
  mESImpl = new EventSourceImpl(this, aCookieJarSettings);
}

EventSource::~EventSource() = default;

nsresult EventSource::CreateAndDispatchSimpleEvent(const nsAString& aName) {
  RefPtr<Event> event = NS_NewDOMEvent(this, nullptr, nullptr);
  // it doesn't bubble, and it isn't cancelable
  event->InitEvent(aName, false, false);
  event->SetTrusted(true);
  ErrorResult rv;
  DispatchEvent(*event, rv);
  return rv.StealNSResult();
}

/* static */
already_AddRefed<EventSource> EventSource::Constructor(
    const GlobalObject& aGlobal, const nsAString& aURL,
    const EventSourceInit& aEventSourceInitDict, ErrorResult& aRv) {
  nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(aGlobal.GetAsSupports());
  if (NS_WARN_IF(!global)) {
    aRv.Throw(NS_ERROR_FAILURE);
    return nullptr;
  }

  nsCOMPtr<nsICookieJarSettings> cookieJarSettings;
  nsCOMPtr<nsPIDOMWindowInner> ownerWindow = do_QueryInterface(global);
  if (ownerWindow) {
    Document* doc = ownerWindow->GetExtantDoc();
    if (NS_WARN_IF(!doc)) {
      aRv.Throw(NS_ERROR_FAILURE);
      return nullptr;
    }

    cookieJarSettings = doc->CookieJarSettings();
  } else {
    // Worker side.
    WorkerPrivate* workerPrivate = GetCurrentThreadWorkerPrivate();
    if (!workerPrivate) {
      aRv.Throw(NS_ERROR_FAILURE);
      return nullptr;
    }

    cookieJarSettings = workerPrivate->CookieJarSettings();
  }

  RefPtr<EventSource> eventSource = new EventSource(
      global, cookieJarSettings, aEventSourceInitDict.mWithCredentials);

  if (NS_IsMainThread()) {
    // Get principal from document and init EventSourceImpl
    nsCOMPtr<nsIScriptObjectPrincipal> scriptPrincipal =
        do_QueryInterface(aGlobal.GetAsSupports());
    if (!scriptPrincipal) {
      aRv.Throw(NS_ERROR_FAILURE);
      return nullptr;
    }
    nsCOMPtr<nsIPrincipal> principal = scriptPrincipal->GetPrincipal();
    if (!principal) {
      aRv.Throw(NS_ERROR_FAILURE);
      return nullptr;
    }
    eventSource->mESImpl->Init(principal, aURL, aRv);
    if (NS_WARN_IF(aRv.Failed())) {
      return nullptr;
    }

    eventSource->mESImpl->InitChannelAndRequestEventSource(true);
    return eventSource.forget();
  }

  // Worker side.
  {
    // Scope for possible failures that need cleanup
    auto guardESImpl = MakeScopeExit([&] { eventSource->mESImpl = nullptr; });

    WorkerPrivate* workerPrivate = GetCurrentThreadWorkerPrivate();
    MOZ_ASSERT(workerPrivate);

    eventSource->mESImpl->mInnerWindowID = workerPrivate->WindowID();

    RefPtr<InitRunnable> initRunnable =
        new InitRunnable(workerPrivate, eventSource->mESImpl, aURL);
    initRunnable->Dispatch(Canceling, aRv);
    if (NS_WARN_IF(aRv.Failed())) {
      return nullptr;
    }

    aRv = initRunnable->ErrorCode();
    if (NS_WARN_IF(aRv.Failed())) {
      return nullptr;
    }

    // In workers we have to keep the worker alive using a WorkerRef in order
    // to dispatch messages correctly.
    // Note, initRunnable->Dispatch may have cleared mESImpl.
    if (!eventSource->mESImpl ||
        !eventSource->mESImpl->CreateWorkerRef(workerPrivate)) {
      // The worker is already shutting down. Let's return an already closed
      // object, but marked as Connecting.
      if (eventSource->mESImpl) {
        // mESImpl is nulled by this call such that EventSourceImpl is
        // released before returning the object, otherwise
        // it will set EventSource to a CLOSED state in its DTOR..
        eventSource->mESImpl->Close();
      }
      eventSource->mReadyState = EventSourceImpl::CONNECTING;

      guardESImpl.release();
      return eventSource.forget();
    }

    // Let's connect to the server.
    RefPtr<ConnectRunnable> connectRunnable =
        new ConnectRunnable(workerPrivate, eventSource->mESImpl);
    connectRunnable->Dispatch(Canceling, aRv);
    if (NS_WARN_IF(aRv.Failed())) {
      return nullptr;
    }

    // End of scope for possible failures
    guardESImpl.release();
  }

  return eventSource.forget();
}

// nsWrapperCache
JSObject* EventSource::WrapObject(JSContext* aCx,
                                  JS::Handle<JSObject*> aGivenProto) {
  return EventSource_Binding::Wrap(aCx, this, aGivenProto);
}

void EventSource::Close() {
  AssertIsOnTargetThread();
  if (mESImpl) {
    // Close potentially kills ourself, ensure
    // to not access any members afterwards.
    mESImpl->Close();
  }
}

//-----------------------------------------------------------------------------
// EventSource::nsISupports
//-----------------------------------------------------------------------------

NS_IMPL_CYCLE_COLLECTION_CLASS(EventSource)

NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(EventSource,
                                                  DOMEventTargetHelper)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END

NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(EventSource,
                                                DOMEventTargetHelper)
  if (tmp->mESImpl) {
    // IsCertainlyaliveForCC will return true and cause the cycle
    // collector to skip this instance when mESImpl is non-null and
    // points back to ourself.
    // mESImpl is initialized to be non-null in the constructor
    // and should have been wiped out in our close function.
    MOZ_ASSERT_UNREACHABLE("Paranoia cleanup that should never happen.");
    tmp->Close();
  }
NS_IMPL_CYCLE_COLLECTION_UNLINK_END

bool EventSource::IsCertainlyAliveForCC() const {
  // Until we are double linked forth and back, we want to stay alive.
  if (!mESImpl) {
    return false;
  }
  auto lock = mESImpl->mSharedData.Lock();
  return lock->mEventSource == this;
}

NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(EventSource)
NS_INTERFACE_MAP_END_INHERITING(DOMEventTargetHelper)

NS_IMPL_ADDREF_INHERITED(EventSource, DOMEventTargetHelper)
NS_IMPL_RELEASE_INHERITED(EventSource, DOMEventTargetHelper)

}  // namespace mozilla::dom
