[gjs/ewlsh/implicit-mainloop: 28/29] Implement implicit mainloop




commit af9250fdf8e787df574d37713423f65b7f75481e
Author: Evan Welsh <contact evanwelsh com>
Date:   Wed Aug 18 02:44:00 2021 -0500

    Implement implicit mainloop

 gjs/context-private.h              |   8 +-
 gjs/context.cpp                    |  78 ++++++++------
 gjs/internal.cpp                   |   9 +-
 gjs/mainloop.cpp                   |  75 +++++++++++++
 gjs/mainloop.h                     |  27 +++++
 gjs/promise.cpp                    | 212 +++++++++++++++++++++++++++++++++++++
 gjs/promise.h                      |  31 ++++++
 installed-tests/js/testMainloop.js |  11 --
 meson.build                        |   2 +
 9 files changed, 406 insertions(+), 47 deletions(-)
---
diff --git a/gjs/context-private.h b/gjs/context-private.h
index 75b04bf0..b28b43b7 100644
--- a/gjs/context-private.h
+++ b/gjs/context-private.h
@@ -17,6 +17,7 @@
 #include <utility>  // for pair
 #include <vector>
 
+#include <gio/gio.h>
 #include <glib-object.h>
 #include <glib.h>
 
@@ -42,6 +43,7 @@ namespace js {
 class SystemAllocPolicy;
 }
 class GjsAtoms;
+class GjsEventLoop;
 class JSTracer;
 
 using JobQueueStorage =
@@ -78,7 +80,9 @@ class GjsContextPrivate : public JS::JobQueue {
     std::vector<std::string> m_args;
 
     JobQueueStorage m_job_queue;
-    unsigned m_idle_drain_handler;
+    GSource* m_promise_queue_source;
+    GCancellable* m_promise_queue_source_cancellable;
+    GjsEventLoop* m_event_loop;
 
     std::vector<std::pair<DestroyNotify, void*>> m_destroy_notifications;
     std::vector<Gjs::Closure::Ptr> m_async_closures;
@@ -134,7 +138,6 @@ class GjsContextPrivate : public JS::JobQueue {
     class SavedQueue;
     void start_draining_job_queue(void);
     void stop_draining_job_queue(void);
-    static gboolean drain_job_queue_idle_handler(void* data);
 
     uint8_t handle_exit_code(const char* type, const char* identifier,
                              GError** error);
@@ -175,6 +178,7 @@ class GjsContextPrivate : public JS::JobQueue {
     [[nodiscard]] JSObject* internal_global() const {
         return m_internal_global.get();
     }
+    [[nodiscard]] GjsEventLoop* event_loop() const { return m_event_loop; }
     [[nodiscard]] GjsProfiler* profiler() const { return m_profiler; }
     [[nodiscard]] const GjsAtoms& atoms() const { return *m_atoms; }
     [[nodiscard]] bool destroying() const { return m_destroying.load(); }
diff --git a/gjs/context.cpp b/gjs/context.cpp
index d8992a58..61a6ce1f 100644
--- a/gjs/context.cpp
+++ b/gjs/context.cpp
@@ -75,16 +75,20 @@
 #include "gjs/importer.h"
 #include "gjs/internal.h"
 #include "gjs/jsapi-util.h"
+#include "gjs/mainloop.h"
 #include "gjs/mem.h"
 #include "gjs/module.h"
 #include "gjs/native.h"
 #include "gjs/objectbox.h"
 #include "gjs/profiler-private.h"
 #include "gjs/profiler.h"
+#include "gjs/promise.h"
 #include "gjs/text-encoding.h"
 #include "modules/modules.h"
 #include "util/log.h"
 
+class GjsEventLoop;
+
 static void     gjs_context_dispose           (GObject               *object);
 static void     gjs_context_finalize          (GObject               *object);
 static void     gjs_context_constructed       (GObject               *object);
@@ -323,6 +327,8 @@ gjs_context_class_init(GjsContextClass *klass)
     g_free (priv_typelib_dir);
     }
 
+    gjs_register_native_module("_promiseNative",
+                               gjs_define_native_promise_stuff);
     gjs_register_native_module("_byteArrayNative", gjs_define_byte_array_stuff);
     gjs_register_native_module("_encodingNative",
                                gjs_define_text_encoding_stuff);
@@ -464,6 +470,7 @@ GjsContextPrivate::~GjsContextPrivate(void) {
     g_clear_pointer(&m_search_path, g_strfreev);
     g_clear_pointer(&m_program_path, g_free);
     g_clear_pointer(&m_program_name, g_free);
+    g_clear_pointer(&m_event_loop, gjs_event_loop_free);
 }
 
 static void
@@ -531,8 +538,8 @@ GjsContextPrivate::GjsContextPrivate(JSContext* cx, GjsContext* public_context)
     : m_public_context(public_context),
       m_cx(cx),
       m_owner_thread(std::this_thread::get_id()),
+      m_event_loop(gjs_event_loop_new()),
       m_environment_preparer(cx) {
-
     JS_SetGCCallback(
         cx,
         [](JSContext*, JSGCStatus status, JS::GCReason reason, void* data) {
@@ -894,32 +901,33 @@ bool GjsContextPrivate::should_exit(uint8_t* exit_code_p) const {
 }
 
 void GjsContextPrivate::start_draining_job_queue(void) {
-    if (!m_idle_drain_handler) {
+    if (!m_promise_queue_source) {
         gjs_debug(GJS_DEBUG_CONTEXT, "Starting promise job queue handler");
-        m_idle_drain_handler = g_idle_add_full(
-            G_PRIORITY_DEFAULT, drain_job_queue_idle_handler, this, nullptr);
+
+        m_promise_queue_source_cancellable = g_cancellable_new();
+        m_promise_queue_source = gjs_promise_job_queue_source_new(
+            this, m_promise_queue_source_cancellable);
+
+        gjs_promise_job_queue_source_attach(m_promise_queue_source);
     }
+
+    gjs_promise_job_queue_source_wakeup(m_promise_queue_source);
 }
 
 void GjsContextPrivate::stop_draining_job_queue(void) {
     m_draining_job_queue = false;
-    if (m_idle_drain_handler) {
-        gjs_debug(GJS_DEBUG_CONTEXT, "Stopping promise job queue handler");
-        g_source_remove(m_idle_drain_handler);
-        m_idle_drain_handler = 0;
+
+    if (m_promise_queue_source_cancellable) {
+        gjs_debug(GJS_DEBUG_CONTEXT, "Cancelling promise job queue handler");
+        g_cancellable_cancel(m_promise_queue_source_cancellable);
     }
-}
 
-gboolean GjsContextPrivate::drain_job_queue_idle_handler(void* data) {
-    gjs_debug(GJS_DEBUG_CONTEXT, "Promise job queue handler");
-    auto* gjs = static_cast<GjsContextPrivate*>(data);
-    gjs->runJobs(gjs->context());
-    /* Uncatchable exceptions are swallowed here - no way to get a handle on
-     * the main loop to exit it from this idle handler */
-    gjs_debug(GJS_DEBUG_CONTEXT, "Promise job queue handler finished");
-    g_assert(gjs->empty() && gjs->m_idle_drain_handler == 0 &&
-             "GjsContextPrivate::runJobs() should have emptied queue");
-    return G_SOURCE_REMOVE;
+    if (m_promise_queue_source) {
+        gjs_debug(GJS_DEBUG_CONTEXT, "Destroying promise job queue handler");
+        gjs_promise_job_queue_source_remove(m_promise_queue_source);
+        m_promise_queue_source = nullptr;
+        m_promise_queue_source_cancellable = nullptr;
+    }
 }
 
 JSObject* GjsContextPrivate::getIncumbentGlobal(JSContext* cx) {
@@ -942,11 +950,6 @@ bool GjsContextPrivate::enqueuePromiseJob(JSContext* cx [[maybe_unused]],
               gjs_debug_object(job).c_str(), gjs_debug_object(promise).c_str(),
               gjs_debug_object(allocation_site).c_str());
 
-    if (m_idle_drain_handler)
-        g_assert(!empty());
-    else
-        g_assert(empty());
-
     if (!m_job_queue.append(job)) {
         JS_ReportOutOfMemory(m_cx);
         return false;
@@ -1031,8 +1034,8 @@ bool GjsContextPrivate::run_jobs_fallible(void) {
         }
     }
 
+    m_draining_job_queue = false;
     m_job_queue.clear();
-    stop_draining_job_queue();
     JS::JobQueueIsEmpty(m_cx);
     return retval;
 }
@@ -1041,14 +1044,12 @@ class GjsContextPrivate::SavedQueue : public JS::JobQueue::SavedJobQueue {
  private:
     GjsContextPrivate* m_gjs;
     JS::PersistentRooted<JobQueueStorage> m_queue;
-    bool m_idle_was_pending : 1;
     bool m_was_draining : 1;
 
  public:
     explicit SavedQueue(GjsContextPrivate* gjs)
         : m_gjs(gjs),
           m_queue(gjs->m_cx, std::move(gjs->m_job_queue)),
-          m_idle_was_pending(gjs->m_idle_drain_handler != 0),
           m_was_draining(gjs->m_draining_job_queue) {
         gjs_debug(GJS_DEBUG_CONTEXT, "Pausing job queue");
         gjs->stop_draining_job_queue();
@@ -1058,8 +1059,7 @@ class GjsContextPrivate::SavedQueue : public JS::JobQueue::SavedJobQueue {
         gjs_debug(GJS_DEBUG_CONTEXT, "Unpausing job queue");
         m_gjs->m_job_queue = std::move(m_queue.get());
         m_gjs->m_draining_job_queue = m_was_draining;
-        if (m_idle_was_pending)
-            m_gjs->start_draining_job_queue();
+        m_gjs->start_draining_job_queue();
     }
 };
 
@@ -1269,12 +1269,17 @@ bool GjsContextPrivate::eval(const char* script, ssize_t script_len,
     JS::RootedValue retval(m_cx);
     bool ok = eval_with_scope(nullptr, script, script_len, filename, &retval);
 
+    if (ok)
+        gjs_event_loop_spin(m_event_loop, m_public_context);
+
     /* The promise job queue should be drained even on error, to finish
      * outstanding async tasks before the context is torn down. Drain after
      * uncaught exceptions have been reported since draining runs callbacks. */
     {
         JS::AutoSaveExceptionState saved_exc(m_cx);
         ok = run_jobs_fallible() && ok;
+
+        stop_draining_job_queue();
     }
 
     auto_profile_exit(auto_profile);
@@ -1285,13 +1290,17 @@ bool GjsContextPrivate::eval(const char* script, ssize_t script_len,
     }
 
     if (exit_status_p) {
+        uint8_t code;
         if (retval.isInt32()) {
             int code = retval.toInt32();
             gjs_debug(GJS_DEBUG_CONTEXT,
                       "Script returned integer code %d", code);
             *exit_status_p = code;
+        } else if (should_exit(&code)) {
+            *exit_status_p = code;
         } else {
-            /* Assume success if no integer was returned */
+            /* Assume success if no integer was returned and should exit isn't
+             * set */
             *exit_status_p = 0;
         }
     }
@@ -1325,9 +1334,10 @@ bool GjsContextPrivate::eval_module(const char* identifier,
         return false;
     }
 
-    bool ok = true;
-    if (!JS::ModuleEvaluate(m_cx, obj))
-        ok = false;
+    bool ok = JS::ModuleEvaluate(m_cx, obj);
+
+    if (ok)
+        gjs_event_loop_spin(m_event_loop, m_public_context);
 
     /* The promise job queue should be drained even on error, to finish
      * outstanding async tasks before the context is torn down. Drain after
@@ -1336,6 +1346,8 @@ bool GjsContextPrivate::eval_module(const char* identifier,
     {
         JS::AutoSaveExceptionState saved_exc(m_cx);
         ok = run_jobs_fallible() && ok;
+
+        stop_draining_job_queue();
     }
 
     auto_profile_exit(auto_profile);
diff --git a/gjs/internal.cpp b/gjs/internal.cpp
index ed856ac4..708c01fe 100644
--- a/gjs/internal.cpp
+++ b/gjs/internal.cpp
@@ -39,6 +39,7 @@
 #include "gjs/importer.h"
 #include "gjs/jsapi-util-args.h"
 #include "gjs/jsapi-util.h"
+#include "gjs/mainloop.h"
 #include "gjs/mem-private.h"
 #include "gjs/module.h"
 #include "gjs/native.h"
@@ -503,7 +504,8 @@ class PromiseData {
 static void load_async_callback(GObject* file, GAsyncResult* res, void* data) {
     std::unique_ptr<PromiseData> promise(PromiseData::from_ptr(data));
 
-    JSAutoRealm ac(promise->cx, gjs_get_import_global(promise->cx));
+    GjsContextPrivate* priv = GjsContextPrivate::from_cx(promise->cx);
+    JSAutoRealm ac(promise->cx, priv->global());
 
     char* contents;
     size_t length;
@@ -516,6 +518,7 @@ static void load_async_callback(GObject* file, GAsyncResult* res, void* data) {
                          error->message);
         g_clear_error(&error);
         promise->reject_with_pending_exception();
+        gjs_event_loop_unref(priv->event_loop());
         return;
     }
 
@@ -524,10 +527,12 @@ static void load_async_callback(GObject* file, GAsyncResult* res, void* data) {
     g_free(contents);
     if (!ok) {
         promise->reject_with_pending_exception();
+
         return;
     }
 
     promise->resolve(text);
+    gjs_event_loop_unref(priv->event_loop());
 }
 
 GJS_JSAPI_RETURN_CONVENTION
@@ -541,6 +546,7 @@ static bool load_async_executor(JSContext* cx, unsigned argc, JS::Value* vp) {
     g_assert(JS_ObjectIsFunction(resolve) && "Executor called weirdly");
     g_assert(JS_ObjectIsFunction(reject) && "Executor called weirdly");
 
+    auto priv = GjsContextPrivate::from_cx(cx);
     JS::Value priv_value = js::GetFunctionNativeReserved(&args.callee(), 0);
     g_assert(!priv_value.isNull() && "Executor called twice");
     GjsAutoUnref<GFile> file = G_FILE(priv_value.toPrivate());
@@ -551,6 +557,7 @@ static bool load_async_executor(JSContext* cx, unsigned argc, JS::Value* vp) {
 
     auto* data = new PromiseData(cx, JS_GetObjectFunction(resolve),
                                  JS_GetObjectFunction(reject));
+    gjs_event_loop_ref(priv->event_loop());
     g_file_load_contents_async(file, nullptr, load_async_callback, data);
 
     args.rval().setUndefined();
diff --git a/gjs/mainloop.cpp b/gjs/mainloop.cpp
new file mode 100644
index 00000000..ebb75a95
--- /dev/null
+++ b/gjs/mainloop.cpp
@@ -0,0 +1,75 @@
+/* -*- mode: C++; c-basic-offset: 4; indent-tabs-mode: nil; -*- */
+// SPDX-License-Identifier: MIT OR LGPL-2.0-or-later
+// SPDX-FileCopyrightText: 2021 Evan Welsh <contact evanwelsh com>
+
+#include <gio/gio.h>
+#include <glib.h>
+
+#include "gjs/context-private.h"
+#include "gjs/mainloop.h"
+
+class GjsEventLoop {
+    uint32_t m_refcount;
+
+ public:
+    GjsEventLoop() { m_refcount = 0; }
+
+    void ref() { m_refcount++; }
+
+    bool unref() {
+        m_refcount--;
+        return true;
+    }
+
+    void spin(GjsContext* context) {
+        auto priv = GjsContextPrivate::from_object(context);
+
+        // Check if System.exit() has been called.
+        if (priv->should_exit(nullptr))
+            return;
+
+        // Whether there are still sources pending.
+        bool has_pending;
+
+        GjsAutoPointer<GMainContext, GMainContext, g_main_context_unref>
+            main_context(g_main_context_ref_thread_default());
+
+        do {
+            if (priv->should_exit(nullptr))
+                break;
+
+            has_pending = g_main_context_pending(main_context);
+
+            // Only run the loop if there are pending jobs.
+            if (has_pending) {
+                // If a source was run, we'll iterate again regardless.
+                has_pending =
+                    g_main_context_iteration(main_context, m_refcount > 0) ||
+                    g_main_context_pending(main_context);
+            }
+
+            // Check if System.exit() has been called.
+            if (priv->should_exit(nullptr))
+                break;
+        } while (
+            // If there are pending sources or the job queue is not empty
+            (m_refcount > 0 || has_pending || !priv->empty()) &&
+            // and System.exit() has not been called
+            // continue spinning the event loop.
+            !priv->should_exit(nullptr));
+    }
+};
+
+GjsEventLoop* gjs_event_loop_new() { return new GjsEventLoop(); }
+
+void gjs_event_loop_free(GjsEventLoop* event_loop) { delete event_loop; }
+
+void gjs_event_loop_spin(GjsEventLoop* event_loop, GjsContext* context) {
+    event_loop->spin(context);
+}
+
+void gjs_event_loop_ref(GjsEventLoop* event_loop) { event_loop->ref(); }
+
+bool gjs_event_loop_unref(GjsEventLoop* event_loop) {
+    return event_loop->unref();
+}
diff --git a/gjs/mainloop.h b/gjs/mainloop.h
new file mode 100644
index 00000000..e85ed00e
--- /dev/null
+++ b/gjs/mainloop.h
@@ -0,0 +1,27 @@
+/* -*- mode: C++; c-basic-offset: 4; indent-tabs-mode: nil; -*- */
+/*
+ * SPDX-License-Identifier: MIT OR LGPL-2.0-or-later
+ * SPDX-FileCopyrightText: 2021 Evan Welsh <contact evanwelsh com>
+ */
+
+#ifndef GJS_MAINLOOP_H_
+#define GJS_MAINLOOP_H_
+
+#include <config.h>
+#include <js/TypeDecls.h>
+
+#include "gjs/context.h"
+
+class GjsEventLoop;
+
+GjsEventLoop* gjs_event_loop_new();
+
+void gjs_event_loop_free(GjsEventLoop* event_loop);
+
+void gjs_event_loop_spin(GjsEventLoop* event_loop, GjsContext* context);
+
+void gjs_event_loop_ref(GjsEventLoop* event_loop);
+
+bool gjs_event_loop_unref(GjsEventLoop* event_loop);
+
+#endif  // GJS_MAINLOOP_H_
diff --git a/gjs/promise.cpp b/gjs/promise.cpp
new file mode 100644
index 00000000..7afb991a
--- /dev/null
+++ b/gjs/promise.cpp
@@ -0,0 +1,212 @@
+/* -*- mode: C++; c-basic-offset: 4; indent-tabs-mode: nil; -*- */
+// SPDX-License-Identifier: MIT OR LGPL-2.0-or-later
+// SPDX-FileCopyrightText: 2021 Evan Welsh <contact evanwelsh com>
+
+#include <config.h>
+
+#include <js/CallArgs.h>
+#include <js/RootingAPI.h>
+#include <js/TypeDecls.h>
+#include <jsapi.h>
+
+#include "gjs/context-private.h"
+#include "gjs/promise.h"
+
+// G_PRIORITY_HIGH is -100, we set -1000 to ensure our source
+// always has the greatest priority. This means our prepare will
+// be called before other sources, and prepare will determine whether
+// we dispatch.
+#define GJS_PROMISE_JOB_QUEUE_SOURCE_PRIORITY -1000
+
+class GjsPromiseJobQueueSource;
+
+typedef gboolean (*GjsPromiseJobQueueSourceFunc)(void* promise_queue_source);
+
+/**
+ * A private class which holds the state for GjsPromiseJobQueueSource
+ * GSources and the GSourceFuncs for the source behavior.
+ */
+class GjsPromiseJobQueueSource {
+ public:
+    // The parent source.
+    GSource parent;
+    // The private GJS context this source runs within.
+    GjsContextPrivate* cx;
+    // The thread-default GMainContext
+    GMainContext* main_context;
+    GCancellable* cancellable;
+    int source_id;
+
+ private:
+    // Called to determine whether the source should run (dispatch) in the
+    // next event loop iteration. If the job queue is not empty we return true
+    // to schedule a dispatch, if the job queue has been empty we quit the main
+    // loop. This should return execution to gjs_spin_event_loop which may
+    // restart the loop if additional jobs are added.
+    static gboolean prepare(GSource* source, gint* timeout [[maybe_unused]]) {
+        auto promise_queue_source =
+            reinterpret_cast<GjsPromiseJobQueueSource*>(source);
+
+        GjsContextPrivate* cx = promise_queue_source->cx;
+        if (!cx->empty())
+            return true;
+
+        g_main_context_wakeup(promise_queue_source->main_context);
+        return false;
+    }
+
+    // If the job queue is empty, dispatch will quit the event loop
+    // otherwise it will drain the job queue. Dispatch must always
+    // return G_SOURCE_CONTINUE, it should never remove the source
+    // from the loop.
+    static gboolean dispatch(GSource* source, GSourceFunc callback,
+                             gpointer data [[maybe_unused]]) {
+        auto promise_queue_source =
+            reinterpret_cast<GjsPromiseJobQueueSource*>(source);
+
+        GjsPromiseJobQueueSourceFunc func =
+            reinterpret_cast<GjsPromiseJobQueueSourceFunc>(callback);
+
+        // The ready time is sometimes set to 0 to kick us out of polling,
+        // we need to reset the value here or this source will always be the
+        // next one to execute. (it will starve the other sources)
+        g_source_set_ready_time(source, -1);
+
+        func(promise_queue_source);
+
+        return G_SOURCE_CONTINUE;
+    }
+
+    // Removes the GjsPrivateContext reference.
+    static void finalize(GSource* source) {
+        auto promise_queue_source =
+            reinterpret_cast<GjsPromiseJobQueueSource*>(source);
+
+        promise_queue_source->cx = nullptr;
+
+        g_main_context_unref(promise_queue_source->main_context);
+        promise_queue_source->main_context = nullptr;
+    }
+
+    static gboolean callback(void* source) {
+        auto promise_queue_source =
+            reinterpret_cast<GjsPromiseJobQueueSource*>(source);
+        if (g_cancellable_is_cancelled(promise_queue_source->cancellable))
+            return G_SOURCE_REMOVE;
+
+        GjsContextPrivate* cx = promise_queue_source->cx;
+        if (cx->empty()) {
+            g_main_context_wakeup(promise_queue_source->main_context);
+        }
+
+        // Drain the job queue.
+        cx->runJobs(cx->context());
+
+        return G_SOURCE_CONTINUE;
+    }
+
+    // g_source_new does not accept const values so
+    // this static member is defined outside of the
+    // class body.
+    static GSourceFuncs source_funcs;
+
+ public:
+    // Creates a new GSource with this class' state and source_funcs.
+    static GSource* create(GjsContextPrivate* cx, GCancellable* cancellable) {
+        g_return_val_if_fail(cx != nullptr, nullptr);
+        g_return_val_if_fail(
+            cancellable == nullptr || G_IS_CANCELLABLE(cancellable), nullptr);
+
+        GSource* source =
+            g_source_new(&source_funcs, sizeof(GjsPromiseJobQueueSource));
+        g_source_set_priority(source, GJS_PROMISE_JOB_QUEUE_SOURCE_PRIORITY);
+        g_source_set_callback(source, &callback, nullptr, nullptr);
+        g_source_set_name(source, "GjsPromiseJobQueueSource");
+
+        // TODO(ewlsh): Do we need this?
+        // g_source_set_can_recurse(source, true);
+        auto promise_queue_source =
+            reinterpret_cast<GjsPromiseJobQueueSource*>(source);
+        promise_queue_source->cx = cx;
+        promise_queue_source->main_context =
+            g_main_context_ref_thread_default();
+        promise_queue_source->source_id = -1;
+        promise_queue_source->cancellable = cancellable;
+
+        g_assert(promise_queue_source->main_context);
+
+        // Add a cancellable source.
+        GSource* cancellable_source = g_cancellable_source_new(cancellable);
+        g_source_set_dummy_callback(cancellable_source);
+        g_source_add_child_source(source, cancellable_source);
+        g_source_unref(cancellable_source);
+
+        return source;
+    }
+};
+
+GSourceFuncs GjsPromiseJobQueueSource::source_funcs = {
+    &GjsPromiseJobQueueSource::prepare,
+    nullptr,
+    &GjsPromiseJobQueueSource::dispatch,
+    &GjsPromiseJobQueueSource::finalize,
+    nullptr,
+    nullptr,
+};
+
+/**
+ * gjs_promise_job_queue_source_new:
+ *
+ * @brief Creates a new GjsPromiseJobQueueSource GSource with an
+ * optional cancellable.
+ *
+ * @param cx the current JSContext
+ * @param cancellable an optional cancellable
+ *
+ * @returns the created source
+ */
+GSource* gjs_promise_job_queue_source_new(GjsContextPrivate* cx,
+                                          GCancellable* cancellable) {
+    return GjsPromiseJobQueueSource::create(cx, cancellable);
+}
+
+void gjs_promise_job_queue_source_attach(GSource* source) {
+    auto promise_queue_source =
+        reinterpret_cast<GjsPromiseJobQueueSource*>(source);
+
+    promise_queue_source->source_id =
+        g_source_attach(source, promise_queue_source->main_context);
+}
+
+void gjs_promise_job_queue_source_remove(GSource* source) {
+    auto promise_queue_source =
+        reinterpret_cast<GjsPromiseJobQueueSource*>(source);
+
+    g_source_remove(promise_queue_source->source_id);
+    g_source_destroy(source);
+    g_source_unref(source);
+}
+
+void gjs_promise_job_queue_source_wakeup(GSource* source) {
+    g_source_set_ready_time(source, 0);
+}
+
+GJS_JSAPI_RETURN_CONVENTION
+static bool run_func(JSContext* cx, unsigned argc, JS::Value* vp) {
+    JS::CallArgs args = JS::CallArgsFromVp(argc, vp);
+
+    auto gjs = GjsContextPrivate::from_cx(cx);
+    gjs->runJobs(cx);
+
+    args.rval().setUndefined();
+    return true;
+}
+
+static JSFunctionSpec gjs_native_promise_module_funcs[] = {
+    JS_FN("run", run_func, 2, 0), JS_FS_END};
+
+bool gjs_define_native_promise_stuff(JSContext* cx,
+                                     JS::MutableHandleObject module) {
+    module.set(JS_NewPlainObject(cx));
+    return JS_DefineFunctions(cx, module, gjs_native_promise_module_funcs);
+}
diff --git a/gjs/promise.h b/gjs/promise.h
new file mode 100644
index 00000000..6608b3af
--- /dev/null
+++ b/gjs/promise.h
@@ -0,0 +1,31 @@
+/* -*- mode: C++; c-basic-offset: 4; indent-tabs-mode: nil; -*- */
+/*
+ * SPDX-License-Identifier: MIT OR LGPL-2.0-or-later
+ * SPDX-FileCopyrightText: 2021 Evan Welsh <contact evanwelsh com>
+ */
+
+#ifndef GJS_PROMISE_H_
+#define GJS_PROMISE_H_
+
+#include <config.h>
+
+#include <gio/gio.h>
+#include <glib.h>
+
+#include <js/TypeDecls.h>
+
+#include "gjs/context-private.h"
+
+GSource* gjs_promise_job_queue_source_new(GjsContextPrivate* cx,
+                                          GCancellable* cancellable);
+
+void gjs_promise_job_queue_source_attach(GSource* source);
+
+void gjs_promise_job_queue_source_remove(GSource* source);
+
+void gjs_promise_job_queue_source_wakeup(GSource* source);
+
+bool gjs_define_native_promise_stuff(JSContext* cx,
+                                     JS::MutableHandleObject module);
+
+#endif  // GJS_PROMISE_H_
diff --git a/installed-tests/js/testMainloop.js b/installed-tests/js/testMainloop.js
index 98dca959..36f67b52 100644
--- a/installed-tests/js/testMainloop.js
+++ b/installed-tests/js/testMainloop.js
@@ -85,15 +85,4 @@ describe('Mainloop.idle_add()', function () {
             return false;
         });
     });
-
-    // Add an idle before exit, then never run main loop again.
-    // This is to test that we remove idle callbacks when the associated
-    // JSContext is blown away. The leak check in minijasmine will
-    // fail if the idle function is not garbage collected.
-    it('does not leak idle callbacks', function () {
-        Mainloop.idle_add(() => {
-            fail('This should never have been called');
-            return true;
-        });
-    });
 });
diff --git a/meson.build b/meson.build
index b6f6b40e..05c896e9 100644
--- a/meson.build
+++ b/meson.build
@@ -401,12 +401,14 @@ libgjs_sources = [
     'gjs/global.cpp', 'gjs/global.h',
     'gjs/importer.cpp', 'gjs/importer.h',
     'gjs/internal.cpp', 'gjs/internal.h',
+    'gjs/mainloop.cpp', 'gjs/mainloop.h',
     'gjs/mem.cpp', 'gjs/mem-private.h',
     'gjs/module.cpp', 'gjs/module.h',
     'gjs/native.cpp', 'gjs/native.h',
     'gjs/objectbox.cpp', 'gjs/objectbox.h',
     'gjs/profiler.cpp', 'gjs/profiler-private.h',
     'gjs/text-encoding.cpp', 'gjs/text-encoding.h',
+    'gjs/promise.cpp', 'gjs/promise.h',
     'gjs/stack.cpp',
     'modules/console.cpp', 'modules/console.h',
     'modules/modules.cpp', 'modules/modules.h',


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]