[gjs/ewlsh/implicit-mainloop] Implement implicit mainloop and timer API.
- From: Evan Welsh <ewlsh src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [gjs/ewlsh/implicit-mainloop] Implement implicit mainloop and timer API.
- Date: Sat, 10 Jul 2021 03:52:41 +0000 (UTC)
commit 3d0e6ca70798ca081a39eed231781e0b06b55ce5
Author: Evan Welsh <contact evanwelsh com>
Date: Wed Mar 31 22:16:18 2021 -0700
Implement implicit mainloop and timer API.
.eslintrc.yml | 4 +
examples/timers.js | 18 ++
gjs/context-private.h | 8 +-
gjs/context.cpp | 77 +++++----
gjs/internal.cpp | 8 +-
gjs/mainloop.cpp | 75 +++++++++
gjs/mainloop.h | 27 +++
gjs/promise.cpp | 212 +++++++++++++++++++++++
gjs/promise.h | 31 ++++
installed-tests/js/meson.build | 1 +
installed-tests/js/minijasmine.js | 17 --
installed-tests/js/testMainloop.js | 21 +--
installed-tests/js/testTimers.js | 317 +++++++++++++++++++++++++++++++++++
js.gresource.xml | 1 +
meson.build | 2 +
modules/core/.eslintrc.yml | 5 +
modules/core/_timers.js | 146 ++++++++++++++++
modules/print.cpp | 31 ++++
modules/script/_bootstrap/default.js | 25 +++
19 files changed, 963 insertions(+), 63 deletions(-)
---
diff --git a/.eslintrc.yml b/.eslintrc.yml
index 7ddf0e38..3511181e 100644
--- a/.eslintrc.yml
+++ b/.eslintrc.yml
@@ -254,5 +254,9 @@ globals:
print: readonly
printerr: readonly
window: readonly
+ setTimeout: readonly
+ setInterval: readonly
+ clearTimeout: readonly
+ clearInterval: readonly
parserOptions:
ecmaVersion: 2020
diff --git a/examples/timers.js b/examples/timers.js
new file mode 100644
index 00000000..092770cc
--- /dev/null
+++ b/examples/timers.js
@@ -0,0 +1,18 @@
+// SPDX-License-Identifier: MIT OR LGPL-2.0-or-later
+// SPDX-FileCopyrightText: 2021 Evan Welsh <contact evanwelsh com>
+
+const promise = new Promise(r => {
+ let i = 100;
+ while (i--)
+ ;
+
+ r();
+});
+
+setTimeout(() => {
+ promise.then(() => log('no'));
+});
+
+setTimeout(() => {
+ log('de');
+});
diff --git a/gjs/context-private.h b/gjs/context-private.h
index 4884f4a4..b9ea21e9 100644
--- a/gjs/context-private.h
+++ b/gjs/context-private.h
@@ -16,6 +16,7 @@
#include <unordered_map>
#include <vector>
+#include <gio/gio.h>
#include <glib-object.h>
#include <glib.h>
@@ -40,6 +41,7 @@ namespace js {
class SystemAllocPolicy;
}
class GjsAtoms;
+class GjsEventLoop;
class JSTracer;
using JobQueueStorage =
@@ -72,7 +74,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::unordered_map<uint64_t, GjsAutoChar> m_unhandled_rejection_stacks;
@@ -126,7 +130,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);
void warn_about_unhandled_promise_rejections(void);
@@ -169,6 +172,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 894e39df..42fa571b 100644
--- a/gjs/context.cpp
+++ b/gjs/context.cpp
@@ -72,6 +72,7 @@
#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"
@@ -79,6 +80,7 @@
#include "gjs/profiler-private.h"
#include "gjs/profiler.h"
#include "gjs/text-encoding.h"
+#include "gjs/promise.h"
#include "modules/modules.h"
#include "util/log.h"
@@ -320,6 +322,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);
@@ -446,6 +450,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
@@ -493,7 +498,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_environment_preparer(cx) {
+ m_environment_preparer(cx),
+ m_event_loop(gjs_event_loop_new()) {
JS_SetGCCallback(
cx,
@@ -861,32 +867,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) {
@@ -909,11 +916,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;
@@ -998,8 +1000,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;
}
@@ -1008,14 +1010,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();
@@ -1025,8 +1025,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();
}
};
@@ -1228,12 +1227,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);
@@ -1244,13 +1248,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;
}
}
@@ -1284,9 +1292,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
@@ -1295,6 +1304,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 a803eda4..5b8cbf7a 100644
--- a/gjs/internal.cpp
+++ b/gjs/internal.cpp
@@ -41,6 +41,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"
@@ -512,7 +513,7 @@ class PromiseData {
static void load_async_callback(GObject* file, GAsyncResult* res, void* data) {
std::unique_ptr<PromiseData> promise(PromiseData::from_ptr(data));
-
+ auto priv = GjsContextPrivate::from_cx(promise->cx);
char* contents;
size_t length;
GError* error = nullptr;
@@ -524,6 +525,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;
}
@@ -532,10 +534,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
@@ -549,6 +553,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());
@@ -559,6 +564,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/meson.build b/installed-tests/js/meson.build
index e11f1418..1405187c 100644
--- a/installed-tests/js/meson.build
+++ b/installed-tests/js/meson.build
@@ -120,6 +120,7 @@ jasmine_tests = [
'Regress',
'Signals',
'System',
+ 'Timers',
'Tweener',
'WarnLib',
]
diff --git a/installed-tests/js/minijasmine.js b/installed-tests/js/minijasmine.js
index a82251a4..c1771c44 100644
--- a/installed-tests/js/minijasmine.js
+++ b/installed-tests/js/minijasmine.js
@@ -19,23 +19,6 @@ function _filterStack(stack) {
.join('\n');
}
-function _setTimeoutInternal(continueTimeout, func, time) {
- return GLib.timeout_add(GLib.PRIORITY_DEFAULT, time, function () {
- func();
- return continueTimeout;
- });
-}
-
-function _clearTimeoutInternal(id) {
- if (id > 0)
- GLib.source_remove(id);
-}
-
-// Install the browser setTimeout/setInterval API on the global object
-globalThis.setTimeout = _setTimeoutInternal.bind(undefined, GLib.SOURCE_REMOVE);
-globalThis.setInterval = _setTimeoutInternal.bind(undefined, GLib.SOURCE_CONTINUE);
-globalThis.clearTimeout = globalThis.clearInterval = _clearTimeoutInternal;
-
let jasmineRequire = imports.jasmine.getJasmineRequireObj();
let jasmineCore = jasmineRequire.core(jasmineRequire);
globalThis._jasmineEnv = jasmineCore.getEnv();
diff --git a/installed-tests/js/testMainloop.js b/installed-tests/js/testMainloop.js
index aae9a140..6fce2d4c 100644
--- a/installed-tests/js/testMainloop.js
+++ b/installed-tests/js/testMainloop.js
@@ -86,14 +86,15 @@ describe('Mainloop.idle_add()', function () {
});
});
- // 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;
- });
- });
+ // TODO(ewlsh): This no longer works with our implicit mainloop.
+ // // 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/installed-tests/js/testTimers.js b/installed-tests/js/testTimers.js
new file mode 100644
index 00000000..1d3398cc
--- /dev/null
+++ b/installed-tests/js/testTimers.js
@@ -0,0 +1,317 @@
+// SPDX-License-Identifier: MIT OR LGPL-2.0-or-later
+// SPDX-FileCopyrightText: 2018-2019 the Deno authors. All rights reserved.
+
+const {GLib} = imports.gi;
+
+function deferred() {
+ let resolve_;
+ let reject_;
+ function resolve() {
+ resolve_();
+ }
+ function reject() {
+ reject_();
+ }
+ const promise = new Promise((res, rej) => {
+ resolve_ = res;
+ reject_ = rej;
+ });
+ return {
+ promise,
+ resolve,
+ reject,
+ };
+}
+
+// eslint-disable-next-line require-await
+async function waitForMs(ms) {
+ return new Promise(resolve => setTimeout(resolve, ms));
+}
+
+describe('Timers', function () {
+ it('times out successfully', async function timeoutSuccess() {
+ const {promise, resolve} = deferred();
+ let count = 0;
+ setTimeout(() => {
+ count++;
+ resolve();
+ }, 500);
+ await promise;
+
+ // count should increment
+ expect(count).toBe(1);
+
+
+ return 5;
+ });
+
+ it('has correct timeout args', async function timeoutArgs() {
+ const {promise, resolve} = deferred();
+ const arg = 1;
+
+ setTimeout(
+ (a, b, c) => {
+ expect(a).toBe(arg);
+ expect(b).toBe(arg.toString());
+ expect(c).toEqual([arg]);
+ resolve();
+ },
+ 10,
+ arg,
+ arg.toString(),
+ [arg]
+ );
+ await promise;
+ });
+
+ it('cancels successfully', async function timeoutCancelSuccess() {
+ let count = 0;
+ const id = setTimeout(() => {
+ count++;
+ }, 1);
+ // Cancelled, count should not increment
+ clearTimeout(id);
+ await waitForMs(600);
+ expect(count).toBe(0);
+ });
+
+ it('cancels multiple correctly', async function timeoutCancelMultiple() {
+ function uncalled() {
+ throw new Error('This function should not be called.');
+ }
+
+ // Set timers and cancel them in the same order.
+ const t1 = setTimeout(uncalled, 10);
+ const t2 = setTimeout(uncalled, 10);
+ const t3 = setTimeout(uncalled, 10);
+ clearTimeout(t1);
+ clearTimeout(t2);
+ clearTimeout(t3);
+
+ // Set timers and cancel them in reverse order.
+ const t4 = setTimeout(uncalled, 20);
+ const t5 = setTimeout(uncalled, 20);
+ const t6 = setTimeout(uncalled, 20);
+ clearTimeout(t6);
+ clearTimeout(t5);
+ clearTimeout(t4);
+
+ // Sleep until we're certain that the cancelled timers aren't gonna fire.
+ await waitForMs(50);
+ });
+
+ it('cancels invalid silent fail', async function timeoutCancelInvalidSilentFail() {
+ // Expect no panic
+ const {promise, resolve} = deferred();
+ let count = 0;
+ const id = setTimeout(() => {
+ count++;
+ // Should have no effect
+ clearTimeout(id);
+ resolve();
+ }, 500);
+ await promise;
+ expect(count).toBe(1);
+
+ // Should silently fail (no panic)
+ clearTimeout(2147483647);
+ });
+
+ it('interval success', async function intervalSuccess() {
+ const {promise, resolve} = deferred();
+ let count = 0;
+ const id = setInterval(() => {
+ count++;
+ clearInterval(id);
+ resolve();
+ }, 100);
+ await promise;
+ // Clear interval
+ clearInterval(id);
+ // count should increment twice
+ expect(count).toBe(1);
+ });
+
+ it('cancels interval successfully', async function intervalCancelSuccess() {
+ let count = 0;
+ const id = setInterval(() => {
+ count++;
+ }, 1);
+ clearInterval(id);
+ await waitForMs(500);
+ expect(count).toBe(0);
+ });
+
+ it('ordering interval', async function intervalOrdering() {
+ const timers = [];
+ let timeouts = 0;
+ function onTimeout() {
+ ++timeouts;
+ for (let i = 1; i < timers.length; i++)
+ clearTimeout(timers[i]);
+ }
+ for (let i = 0; i < 10; i++)
+ timers[i] = setTimeout(onTimeout, 1);
+
+ await waitForMs(500);
+ expect(timeouts).toBe(1);
+ });
+
+ it('cancel invalid silent fail',
+ // eslint-disable-next-line require-await
+ async function intervalCancelInvalidSilentFail() {
+ // Should silently fail (no panic)
+ clearInterval(2147483647);
+ });
+
+ it('fire immediately', async function fireCallbackImmediatelyWhenDelayOverMaxValue() {
+ GLib.test_expect_message('Gjs', GLib.LogLevelFlags.LEVEL_WARNING,
+ '*does not fit into*');
+
+ let count = 0;
+ setTimeout(() => {
+ count++;
+ }, 2 ** 31);
+ await waitForMs(1);
+ expect(count).toBe(1);
+ });
+
+ it('callback this', async function timeoutCallbackThis() {
+ const {promise, resolve} = deferred();
+ const obj = {
+ foo() {
+ expect(this).toBe(window);
+ resolve();
+ },
+ };
+ setTimeout(obj.foo, 1);
+ await promise;
+ });
+
+ it('bind this',
+ // eslint-disable-next-line require-await
+ async function timeoutBindThis() {
+ function noop() { }
+
+ const thisCheckPassed = [null, undefined, window, globalThis];
+
+ const thisCheckFailed = [
+ 0,
+ '',
+ true,
+ false,
+ {},
+ [],
+ 'foo',
+ () => { },
+ Object.prototype,
+ ];
+
+ thisCheckPassed.forEach(
+ thisArg => {
+ expect(() => {
+ setTimeout.call(thisArg, noop, 1);
+ }).not.toThrow();
+ });
+
+ thisCheckFailed.forEach(
+ thisArg => {
+ expect(() => {
+ setTimeout.call(thisArg, noop, 1);
+ }).toThrowError(TypeError);
+ }
+ );
+ });
+
+ it('clearTimeout converts to number',
+ // eslint-disable-next-line require-await
+ async function clearTimeoutShouldConvertToNumber() {
+ let called = false;
+ const obj = {
+ valueOf() {
+ called = true;
+ return 1;
+ },
+ };
+ clearTimeout(obj);
+ expect(called).toBe(true);
+ });
+
+ it('throw on bigint', function setTimeoutShouldThrowWithBigint() {
+ expect(() => {
+ setTimeout(() => { }, 1n);
+ }).toThrowError(TypeError);
+ });
+
+ it('throw on bigint', function clearTimeoutShouldThrowWithBigint() {
+ expect(() => {
+ clearTimeout(1n);
+ }).toThrowError(TypeError);
+ });
+
+ it('', function testFunctionName() {
+ expect(clearTimeout.name).toBe('clearTimeout');
+ expect(clearInterval.name).toBe('clearInterval');
+ });
+
+ it('length', function testFunctionParamsLength() {
+ expect(setTimeout.length).toBe(1);
+ expect(setInterval.length).toBe(1);
+ expect(clearTimeout.length).toBe(0);
+ expect(clearInterval.length).toBe(0);
+ });
+
+ it('clear and interval', function clearTimeoutAndClearIntervalNotBeEquals() {
+ expect(clearTimeout).not.toBe(clearInterval);
+ });
+
+ it('microtask ordering', async function timerBasicMicrotaskOrdering() {
+ let s = '';
+ let count = 0;
+ const {promise, resolve} = deferred();
+ setTimeout(() => {
+ Promise.resolve().then(() => {
+ count++;
+ s += 'de';
+ if (count === 2)
+ resolve();
+ });
+ });
+ setTimeout(() => {
+ count++;
+ s += 'no';
+ if (count === 2)
+ resolve();
+ });
+ await promise;
+ expect(s).toBe('deno');
+ });
+
+ it('nested microtask ordering', async function timerNestedMicrotaskOrdering() {
+ let s = '';
+ const {promise, resolve} = deferred();
+ s += '0';
+ setTimeout(() => {
+ s += '4';
+ setTimeout(() => (s += '8'));
+ Promise.resolve().then(() => {
+ setTimeout(() => {
+ s += '9';
+ resolve();
+ });
+ });
+ });
+ setTimeout(() => (s += '5'));
+ Promise.resolve().then(() => (s += '2'));
+ Promise.resolve().then(() =>
+ setTimeout(() => {
+ s += '6';
+ Promise.resolve().then(() => (s += '7'));
+ })
+ );
+ Promise.resolve().then(() => Promise.resolve().then(() => (s += '3')));
+ s += '1';
+ await promise;
+ expect(s).toBe('0123456789');
+ });
+});
diff --git a/js.gresource.xml b/js.gresource.xml
index fc55e597..226ec59f 100644
--- a/js.gresource.xml
+++ b/js.gresource.xml
@@ -45,5 +45,6 @@
<file>modules/core/_format.js</file>
<file>modules/core/_gettext.js</file>
<file>modules/core/_signals.js</file>
+ <file>modules/core/_timers.js</file>
</gresource>
</gresources>
diff --git a/meson.build b/meson.build
index 1a8a0b63..d802ae26 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',
diff --git a/modules/core/.eslintrc.yml b/modules/core/.eslintrc.yml
index 6c9c0253..037dcc81 100644
--- a/modules/core/.eslintrc.yml
+++ b/modules/core/.eslintrc.yml
@@ -3,3 +3,8 @@
# SPDX-FileCopyrightText: 2020 Evan Welsh <contact evanwelsh com>
rules:
jsdoc/require-jsdoc: 'off'
+globals:
+ setTimeout: off
+ setInterval: off
+ clearTimeout: off
+ clearInterval: off
diff --git a/modules/core/_timers.js b/modules/core/_timers.js
new file mode 100644
index 00000000..c5e3eabf
--- /dev/null
+++ b/modules/core/_timers.js
@@ -0,0 +1,146 @@
+// SPDX-License-Identifier: MIT OR LGPL-2.0-or-later
+// SPDX-FileCopyrightText: 2021 Evan Welsh <contact evanwelsh com>
+
+/* exported setTimeout, setInterval, clearTimeout, clearInterval */
+
+const {GLib} = imports.gi;
+
+const jobs = imports._promiseNative;
+
+// It should not be possible to remove or destroy sources from outside this library.
+const ids = new Map();
+const releasedIds = [];
+let idIncrementor = 1;
+
+/**
+ * @param {number} sourceId the source ID to generate a timer ID for
+ * @returns {number}
+ */
+function nextId(sourceId) {
+ let id;
+
+ if (releasedIds.length > 0) {
+ id = releasedIds.pop();
+ } else {
+ idIncrementor++;
+
+ id = idIncrementor;
+ }
+
+ ids.set(id, sourceId);
+ return id;
+}
+
+function releaseId(sourceId) {
+ ids.delete(sourceId);
+ releasedIds.push(sourceId);
+}
+
+const TIMEOUT_MAX = 2 ** 31 - 1;
+
+function checkThis(thisArg) {
+ if (thisArg !== null && thisArg !== undefined && thisArg !== globalThis)
+ throw new TypeError('Illegal invocation');
+}
+
+function checkBigInt(n) {
+ if (typeof n === 'bigint')
+ throw new TypeError('Cannot convert a BigInt value to a number');
+}
+
+function ToNumber(interval) {
+ /* eslint-disable no-implicit-coercion */
+ if (typeof interval === 'number')
+ return interval;
+ else if (typeof interval === 'object')
+ return +interval.valueOf() || +interval;
+
+
+ return +interval;
+ /* eslint-enable */
+}
+
+function setTimeout(callback, delay = 0, ...args) {
+ checkThis(this);
+ checkBigInt(delay);
+
+ delay = wrapDelay(delay);
+ const cb = callback.bind(globalThis, ...args);
+ const id = nextId(GLib.timeout_add(GLib.PRIORITY_DEFAULT, delay, () => {
+ if (!ids.has(id))
+ return GLib.SOURCE_REMOVE;
+
+
+ cb();
+ releaseId(id);
+ // Drain the microtask queue.
+ jobs.run();
+
+
+ return GLib.SOURCE_REMOVE;
+ }));
+
+ return id;
+}
+
+function wrapDelay(delay) {
+ if (delay > TIMEOUT_MAX) {
+ imports._print.warn(
+ `${delay} does not fit into` +
+ ' a 32-bit signed integer.' +
+ '\nTimeout duration was set to 1.'
+ );
+ delay = 1;
+ }
+ return Math.max(0, delay | 0);
+}
+
+function setInterval(callback, delay = 0, ...args) {
+ checkThis(this);
+ checkBigInt(delay);
+
+ delay = wrapDelay(delay);
+ const cb = callback.bind(globalThis, ...args);
+ const id = nextId(GLib.timeout_add(GLib.PRIORITY_DEFAULT, delay, () => {
+ if (!ids.has(id))
+ return GLib.SOURCE_REMOVE;
+
+
+ cb();
+
+ // Drain the microtask queue.
+ jobs.run();
+
+ return GLib.SOURCE_CONTINUE;
+ }));
+
+ return id;
+}
+
+function _clearTimer(id) {
+ checkBigInt(id);
+
+ const _id = ToNumber(id);
+
+ if (!ids.has(_id))
+ return;
+
+
+ const cx = GLib.MainContext.default();
+ const source_id = ids.get(_id);
+ const source = cx.find_source_by_id(source_id);
+
+ if (source_id > 0 && source) {
+ GLib.source_remove(source_id);
+ source.destroy();
+ releaseId(_id);
+ }
+}
+
+function clearTimeout(id = 0) {
+ _clearTimer(id);
+}
+
+function clearInterval(id = 0) {
+ _clearTimer(id);
+}
diff --git a/modules/print.cpp b/modules/print.cpp
index 942fd0a1..e1da76d3 100644
--- a/modules/print.cpp
+++ b/modules/print.cpp
@@ -51,6 +51,36 @@ static bool gjs_log(JSContext* cx, unsigned argc, JS::Value* vp) {
return true;
}
+GJS_JSAPI_RETURN_CONVENTION
+static bool gjs_warn(JSContext* cx, unsigned argc, JS::Value* vp) {
+ JS::CallArgs argv = JS::CallArgsFromVp(argc, vp);
+
+ if (argc != 1) {
+ gjs_throw(cx, "Must pass a single argument to warn()");
+ return false;
+ }
+
+ /* JS::ToString might throw, in which case we will only log that the value
+ * could not be converted to string */
+ JS::AutoSaveExceptionState exc_state(cx);
+ JS::RootedString jstr(cx, JS::ToString(cx, argv[0]));
+ exc_state.restore();
+
+ if (!jstr) {
+ g_message("JS LOG: <cannot convert value to string>");
+ return true;
+ }
+
+ JS::UniqueChars s(JS_EncodeStringToUTF8(cx, jstr));
+ if (!s)
+ return false;
+
+ g_warning("%s", s.get());
+
+ argv.rval().setUndefined();
+ return true;
+}
+
GJS_JSAPI_RETURN_CONVENTION
static bool gjs_log_error(JSContext* cx, unsigned argc, JS::Value* vp) {
JS::CallArgs argv = JS::CallArgsFromVp(argc, vp);
@@ -137,6 +167,7 @@ static bool gjs_printerr(JSContext* context, unsigned argc, JS::Value* vp) {
// clang-format off
static constexpr JSFunctionSpec funcs[] = {
JS_FN("log", gjs_log, 1, GJS_MODULE_PROP_FLAGS),
+ JS_FN("warn", gjs_warn, 1, GJS_MODULE_PROP_FLAGS),
JS_FN("logError", gjs_log_error, 2, GJS_MODULE_PROP_FLAGS),
JS_FN("print", gjs_print, 0, GJS_MODULE_PROP_FLAGS),
JS_FN("printerr", gjs_printerr, 0, GJS_MODULE_PROP_FLAGS),
diff --git a/modules/script/_bootstrap/default.js b/modules/script/_bootstrap/default.js
index 952d7fe3..f000734b 100644
--- a/modules/script/_bootstrap/default.js
+++ b/modules/script/_bootstrap/default.js
@@ -6,6 +6,7 @@
'use strict';
const {print, printerr, log, logError} = imports._print;
+ const {setTimeout, setInterval, clearTimeout, clearInterval} = imports._timers;
Object.defineProperties(exports, {
ARGV: {
@@ -16,6 +17,30 @@
return imports.system.programArgs;
},
},
+ setTimeout: {
+ configurable: false,
+ enumerable: true,
+ writable: true,
+ value: setTimeout,
+ },
+ setInterval: {
+ configurable: false,
+ enumerable: true,
+ writable: true,
+ value: setInterval,
+ },
+ clearTimeout: {
+ configurable: false,
+ enumerable: true,
+ writable: true,
+ value: clearTimeout,
+ },
+ clearInterval: {
+ configurable: false,
+ enumerable: true,
+ writable: true,
+ value: clearInterval,
+ },
print: {
configurable: false,
enumerable: true,
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]