| // Copyright 2026 The Dawn & Tint Authors |
| // |
| // Redistribution and use in source and binary forms, with or without |
| // modification, are permitted provided that the following conditions are met: |
| // |
| // 1. Redistributions of source code must retain the above copyright notice, this |
| // list of conditions and the following disclaimer. |
| // |
| // 2. Redistributions in binary form must reproduce the above copyright notice, |
| // this list of conditions and the following disclaimer in the documentation |
| // and/or other materials provided with the distribution. |
| // |
| // 3. Neither the name of the copyright holder nor the names of its |
| // contributors may be used to endorse or promote products derived from |
| // this software without specific prior written permission. |
| // |
| // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" |
| // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE |
| // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE |
| // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE |
| // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL |
| // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR |
| // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER |
| // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, |
| // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
| // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| |
| #ifndef SRC_DAWN_NODE_STANDALONE_EVENTLOOP_H_ |
| #define SRC_DAWN_NODE_STANDALONE_EVENTLOOP_H_ |
| |
| #include <chrono> |
| #include <cstdint> |
| #include <deque> |
| #include <optional> |
| #include <thread> |
| #include <utility> |
| |
| #include "absl/container/btree_map.h" |
| #include "absl/functional/any_invocable.h" |
| |
| namespace v8 { |
| class Isolate; |
| class Platform; |
| } // namespace v8 |
| |
| namespace dawn::node::standalone { |
| |
| // A Node.js-shaped event loop. Each iteration runs the timers that have come due, then the |
| // immediates that were already queued when the phase began. |
| // |
| // Every task runs inside a handle scope, with the microtask queue drained afterwards, so a task is |
| // free to call into JavaScript. An uncaught exception is printed and stops the loop with exit code |
| // 1, which is why the loop rather than its caller owns the exit code. |
| // |
| // Node's phase order is not an implementation detail we are free to change. `setTimeout` and |
| // `setImmediate` callbacks observe it, and Dawn's AsyncRunner relies on the check phase deferring |
| // work to the next iteration; see the comment on RunOneIteration(). The order is documented at |
| // https://nodejs.org/en/learn/asynchronous-work/event-loop-timers-and-nexttick |
| // |
| // Every method must be called on the thread that constructed the loop, which is the thread the |
| // isolate runs on. Tasks only ever arrive by way of a JavaScript call - the timer polyfills, and |
| // AsyncRunner, which posts by calling the global setImmediate() rather than by reaching in here - |
| // and a JavaScript call is already confined to that thread. Debug builds check this. |
| class EventLoop { |
| public: |
| using Task = absl::AnyInvocable<void()>; |
| using Clock = std::chrono::steady_clock; |
| using TimePoint = Clock::time_point; |
| using Duration = Clock::duration; |
| using TimerId = uint64_t; |
| |
| // What a single iteration found to do. |
| enum class IterationResult { |
| // At least one task ran. More may already be ready. |
| kRanTasks, |
| // Nothing was ready to run. The caller may wait until NextDueTime(). |
| kIdle, |
| // Stop() has been called. |
| kStopped, |
| }; |
| |
| // `isolate` must be entered, with a context entered, whenever a task runs. `platform` is the |
| // one the isolate was created with; the loop pumps its foreground work each iteration. Neither |
| // is owned, and both must outlive the loop. |
| EventLoop(v8::Isolate* isolate, v8::Platform* platform); |
| virtual ~EventLoop(); |
| |
| EventLoop(const EventLoop&) = delete; |
| EventLoop& operator=(const EventLoop&) = delete; |
| |
| // Queues `task` for the next check phase. Backs setImmediate(). |
| void PostTask(Task task); |
| |
| // Queues `task` to run once `delay` has elapsed, and returns an identifier that |
| // CancelDelayedTask() accepts. Backs setTimeout() and setInterval(). |
| TimerId PostDelayedTask(Task task, Duration delay); |
| |
| // Drops a delayed task that has not run yet. Unknown and already-run identifiers are ignored, |
| // because clearTimeout() on an expired timer is not an error. |
| void CancelDelayedTask(TimerId id); |
| |
| // Runs one timers phase followed by one check phase. |
| IterationResult RunOneIteration(); |
| |
| // Runs iterations until Stop() is called or there is no work left to wait for. |
| void Run(); |
| |
| // Asks the loop to finish. Takes effect between tasks. Once stopped, later calls do not |
| // overwrite the exit code. Backs process.exit(). |
| void Stop(int exit_code); |
| |
| bool stopped() const { return stopped_; } |
| int exit_code() const { return exit_code_; } |
| |
| // When the earliest pending timer comes due, or nullopt when no timer is pending. |
| std::optional<TimePoint> NextDueTime() const { |
| if (timers_.empty()) { |
| return std::nullopt; |
| } |
| return timers_.begin()->first.due; |
| } |
| |
| protected: |
| // The loop's only reading of the clock. Virtual so that tests can control the passage of time |
| // without depending on real time; every other seam this class used to have is gone. |
| virtual TimePoint Now() const; |
| |
| private: |
| // Runs a single task and then drains the microtask queue, mirroring what a browser or Node.js |
| // does between task callbacks. An uncaught exception is reported and stops the loop. |
| void RunTask(Task task); |
| |
| // Blocks until `due`, or until it is worth looking for work again. Capped so that |
| // PumpEngineTasks() gets a turn while a distant timer is pending. |
| void WaitUntil(TimePoint due); |
| |
| // Runs any foreground work V8 has queued for this thread, reporting whether it ran any. |
| bool PumpEngineTasks(); |
| |
| // Checks, in debug builds, that the caller is on the thread that constructed the loop. |
| void AssertOnLoopThread() const; |
| |
| // Orders timers by the time they come due, and timers that share a deadline by the order they |
| // were posted, since identifiers are handed out in that order. |
| struct TimerKey { |
| TimePoint due; |
| TimerId id; |
| auto operator<=>(const TimerKey&) const = default; |
| }; |
| |
| v8::Isolate* const isolate_; |
| v8::Platform* const platform_; |
| // The thread the loop belongs to. Only read by AssertOnLoopThread(). |
| const std::thread::id thread_id_ = std::this_thread::get_id(); |
| // Tasks waiting for the next check phase, in the order they were posted. |
| std::deque<Task> immediates_; |
| absl::btree_map<TimerKey, Task> timers_; |
| TimerId next_timer_id_ = 1; |
| bool stopped_ = false; |
| int exit_code_ = 0; |
| }; |
| |
| } // namespace dawn::node::standalone |
| |
| #endif // SRC_DAWN_NODE_STANDALONE_EVENTLOOP_H_ |