/* 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/. */

// nsIEventTarget wrapper for throttling event dispatch.

#ifndef mozilla_ThrottledEventQueue_h
#define mozilla_ThrottledEventQueue_h

#include "nsISerialEventTarget.h"

#define NS_THROTTLEDEVENTQUEUE_IID \
  {0x8f3cf7dc, 0xfc14, 0x4ad5, {0x9f, 0xd5, 0xdb, 0x79, 0xbc, 0xe6, 0xd5, 0x08}}

namespace mozilla {

// A ThrottledEventQueue is an event target that can be used to throttle
// events being dispatched to another base target.  It maintains its
// own queue of events and only dispatches one at a time to the wrapped
// target.  This can be used to avoid flooding the base target.
//
// Flooding is avoided via a very simple principle.  Runnables dispatched
// to the ThrottledEventQueue are only dispatched to the base target
// one at a time.  Only once that runnable has executed will we dispatch
// the next runnable to the base target.  This in effect makes all
// runnables passing through the ThrottledEventQueue yield to other work
// on the base target.
//
// ThrottledEventQueue keeps runnables waiting to be dispatched to the
// base in its own internal queue.  Code can query the length of this
// queue using IsEmpty() and Length().  Further, code implement back
// pressure by checking the depth of the queue and deciding to stop
// issuing runnables if they see the ThrottledEventQueue is backed up.
// Code running on other threads could even use AwaitIdle() to block
// all operation until the ThrottledEventQueue drains.
//
// Note, this class is similar to TaskQueue, but also differs in a few
// ways.  First, it is a very simple nsIEventTarget implementation.  It
// does not use the AbstractThread API.
//
// In addition, ThrottledEventQueue currently dispatches its next
// runnable to the base target *before* running the current event.  This
// allows the event code to spin the event loop without stalling the
// ThrottledEventQueue.  In contrast, TaskQueue only dispatches its next
// runnable after running the current event.  That approach is necessary
// for TaskQueue in order to work with thread pool targets.
//
// So, if you are targeting a thread pool you probably want a TaskQueue.
// If you are targeting a single thread or other non-concurrent event
// target, you probably want a ThrottledEventQueue.
//
// If you drop a ThrottledEventQueue while its queue still has events to be run,
// they will continue to be dispatched as usual to the base. Only once the last
// event has run will all the ThrottledEventQueue's memory be freed.
class ThrottledEventQueue final : public nsISerialEventTarget {
  class Inner;
  RefPtr<Inner> mInner;

  explicit ThrottledEventQueue(already_AddRefed<Inner> aInner);
  ~ThrottledEventQueue() = default;

 public:
  // Create a ThrottledEventQueue for the given target.
  static already_AddRefed<ThrottledEventQueue> Create(
      nsISerialEventTarget* aBaseTarget, const char* aName,
      uint32_t aPriority = nsIRunnablePriority::PRIORITY_NORMAL);

  // Determine if there are any events pending in the queue.
  bool IsEmpty() const;

  // Determine how many events are pending in the queue.
  uint32_t Length() const;

  already_AddRefed<nsIRunnable> GetEvent();

  // Block the current thread until the queue is empty. This may not be called
  // on the main thread or the base target. The ThrottledEventQueue must not be
  // paused.
  void AwaitIdle() const;

  // If |aIsPaused| is true, pause execution of events from this queue. No
  // events from this queue will be run until this is called with |aIsPaused|
  // false.
  //
  // To un-pause a ThrottledEventQueue, we need to dispatch a runnable to the
  // underlying event target. That operation may fail, so this method is
  // fallible as well.
  //
  // Note that, although ThrottledEventQueue's behavior is descibed as queueing
  // events on the base target, an event queued on a TEQ is never actually moved
  // to any other queue. What is actually dispatched to the base is an
  // "executor" event which, when run, removes an event from the TEQ and runs it
  // immediately. This means that you can pause a TEQ even after the executor
  // has been queued on the base target, and even so, no events from the TEQ
  // will run. When the base target gets around to running the executor, the
  // executor will see that the TEQ is paused, and do nothing.
  [[nodiscard]] nsresult SetIsPaused(bool aIsPaused);

  // Return true if this ThrottledEventQueue is paused.
  bool IsPaused() const;

  NS_DECL_THREADSAFE_ISUPPORTS
  NS_DECL_NSIEVENTTARGET_FULL

  NS_INLINE_DECL_STATIC_IID(NS_THROTTLEDEVENTQUEUE_IID);
};

}  // namespace mozilla

#endif  // mozilla_ThrottledEventQueue_h
