[gjs/ewlsh/workers-api: 3/4] Implement Workers API




commit 0fef5a669709a1aa1310d9c35577f79b436217db
Author: Evan Welsh <contact evanwelsh com>
Date:   Fri Mar 25 17:31:07 2022 -0700

    Implement Workers API

 doc/Modules.md          |   8 +-
 gi/gobject.cpp          |   2 +-
 gi/object.cpp           |  10 +-
 gi/object.h             |   4 +-
 gi/value.cpp            |   9 +
 gjs/context-private.h   |  13 ++
 gjs/context.cpp         | 112 ++++++++++--
 gjs/coverage.cpp        |  17 +-
 gjs/engine.cpp          |  27 ++-
 gjs/global.cpp          | 156 ++++++++++++-----
 gjs/global.h            |  13 +-
 gjs/native.cpp          |  14 +-
 gjs/thread.cpp          | 447 ++++++++++++++++++++++++++++++++++++++++++++++++
 gjs/thread.h            |  16 ++
 meson.build             |   1 +
 modules/esm/_timers.js  |   5 +-
 modules/esm/_workers.js | 118 +++++++++++++
 modules/esm/events.js   | 392 ++++++++++++++++++++++++++++++++++++++++++
 18 files changed, 1268 insertions(+), 96 deletions(-)
---
diff --git a/doc/Modules.md b/doc/Modules.md
index f176b2cf8..10a6952ed 100644
--- a/doc/Modules.md
+++ b/doc/Modules.md
@@ -1,6 +1,6 @@
 GJS includes some built-in modules, as well as helpers for some core APIs like DBus like Variants. The 
headings below are links to the JavaScript source, which are decently documented and informative of usage.
 
-## [Gio](https://gitlab.gnome.org/GNOME/gjs/blob/HEAD/modules/core/overrides/Gio.js)
+## [Gio](https://gitlab.gnome.org/GNOME/gjs/blob/HEAD/modules/overrides/Gio.js)
 
 **Import with `const Gio = gi.require('Gio');` or `import Gio from 'gi://Gio'`**
 
@@ -31,7 +31,7 @@ The `Gio` override includes a number of utilities for DBus that will be document
 
 [old-dbus-example]: https://wiki.gnome.org/Gjs/Examples/DBusClient
 
-## [GLib](https://gitlab.gnome.org/GNOME/gjs/blob/HEAD/modules/core/overrides/GLib.js)
+## [GLib](https://gitlab.gnome.org/GNOME/gjs/blob/HEAD/modules/overrides/GLib.js)
 
 **Import with `const GLib = gi.require('GLib');` or `import GLib from 'gi://GLib'`**
 
@@ -42,13 +42,13 @@ Mostly GVariant and GBytes compatibility.
 * `GLib.Variant.unpack()`: Unpack a variant to a native type
 * `GLib.Variant.deep_unpack()`: Deep unpack a variant.
 
-## [GObject](https://gitlab.gnome.org/GNOME/gjs/blob/HEAD/modules/core/overrides/GObject.js)
+## [GObject](https://gitlab.gnome.org/GNOME/gjs/blob/HEAD/modules/overrides/GObject.js)
 
 **Import with `const GObject = gi.require('GObject');` or `import GObject from 'gi://GObject'`**
 
 Mostly GObject implementation (properties, signals, GType mapping). May be useful as a reference.
 
-## [Gtk](https://gitlab.gnome.org/GNOME/gjs/blob/HEAD/modules/core/overrides/Gtk.js)
+## [Gtk](https://gitlab.gnome.org/GNOME/gjs/blob/HEAD/modules/overrides/Gtk.js)
 
 **Import with `const Gtk = gi.require('Gtk', '3.0');` or `import Gtk from 'gi://Gtk'`**
 
diff --git a/gi/gobject.cpp b/gi/gobject.cpp
index 2ab1f28ba..97c69d7fa 100644
--- a/gi/gobject.cpp
+++ b/gi/gobject.cpp
@@ -30,7 +30,7 @@
 static std::unordered_map<GType, AutoParamArray> class_init_properties;
 
 [[nodiscard]] static JSContext* current_js_context() {
-    GjsContext* gjs = gjs_context_get_current();
+    GjsContext* gjs = gjs_context_get_current_thread();
     return static_cast<JSContext*>(gjs_context_get_native_context(gjs));
 }
 
diff --git a/gi/object.cpp b/gi/object.cpp
index 4c5a9efd5..028493c0a 100644
--- a/gi/object.cpp
+++ b/gi/object.cpp
@@ -83,9 +83,9 @@ static_assert(sizeof(ObjectInstance) <= 48,
               "gnome-shell run.");
 #endif  // x86-64 clang
 
-bool ObjectInstance::s_weak_pointer_callback = false;
-decltype(ObjectInstance::s_wrapped_gobject_list)
-    ObjectInstance::s_wrapped_gobject_list;
+bool thread_local ObjectInstance::s_weak_pointer_callback = false;
+decltype(ObjectInstance::s_wrapped_gobject_list) thread_local ObjectInstance::
+    s_wrapped_gobject_list;
 
 static const auto DISPOSED_OBJECT = std::numeric_limits<uintptr_t>::max();
 
@@ -1249,7 +1249,7 @@ ObjectInstance::gobj_dispose_notify(void)
         m_uses_toggle_ref = false;
     }
 
-    if (GjsContextPrivate::from_current_context()->is_owner_thread())
+    if (GjsContextPrivate::from_main_thread_context()->is_owner_thread())
         discard_wrapper();
 }
 
@@ -1389,7 +1389,7 @@ void ObjectInstance::wrapped_gobj_toggle_notify(void* instance, GObject*,
     bool toggle_up_queued, toggle_down_queued;
     auto* self = static_cast<ObjectInstance*>(instance);
 
-    GjsContextPrivate* gjs = GjsContextPrivate::from_current_context();
+    GjsContextPrivate* gjs = GjsContextPrivate::from_main_thread_context();
     if (gjs->destroying()) {
         /* Do nothing here - we're in the process of disassociating
          * the objects.
diff --git a/gi/object.h b/gi/object.h
index 886f13eaf..a6fb604c5 100644
--- a/gi/object.h
+++ b/gi/object.h
@@ -306,7 +306,7 @@ class ObjectInstance : public GIWrapperInstance<ObjectBase, ObjectPrototype,
      * hard ref on the underlying GObject, and may be finalized at will. */
     bool m_uses_toggle_ref : 1;
 
-    static bool s_weak_pointer_callback;
+    static thread_local bool s_weak_pointer_callback;
 
     /* Constructors */
 
@@ -401,7 +401,7 @@ class ObjectInstance : public GIWrapperInstance<ObjectBase, ObjectPrototype,
     /* Methods to manipulate the linked list of instances */
 
  private:
-    static std::vector<ObjectInstance*> s_wrapped_gobject_list;
+    static thread_local std::vector<ObjectInstance*> s_wrapped_gobject_list;
     void link(void);
     void unlink(void);
     [[nodiscard]] static size_t num_wrapped_gobjects() {
diff --git a/gi/value.cpp b/gi/value.cpp
index ecc65ad7f..74a52e55d 100644
--- a/gi/value.cpp
+++ b/gi/value.cpp
@@ -136,6 +136,15 @@ void Gjs::Closure::marshal(GValue* return_value, unsigned n_param_values,
     }
 
     context = m_cx;
+
+    // Prevent calling JavaScript functions from the incorrect thread
+    if (!js::CurrentThreadCanAccessZone(js::GetContextZone(context))) {
+        g_critical(
+            "Attempted to call a JavaScript function from an incorrect "
+            "thread.");
+        return;
+    }
+
     GjsContextPrivate* gjs = GjsContextPrivate::from_cx(context);
     if (G_UNLIKELY(gjs->sweeping())) {
         GSignalInvocationHint *hint = (GSignalInvocationHint*) invocation_hint;
diff --git a/gjs/context-private.h b/gjs/context-private.h
index 3bc7d50fa..42f091030 100644
--- a/gjs/context-private.h
+++ b/gjs/context-private.h
@@ -58,6 +58,10 @@ using GTypeTable =
     JS::GCHashMap<GType, JS::Heap<JSObject*>, js::DefaultHasher<GType>,
                   js::SystemAllocPolicy>;
 
+[[nodiscard]] GjsContext* gjs_context_new_worker();
+[[nodiscard]] GjsContext* gjs_context_get_current_thread();
+[[nodiscard]] GjsContext* gjs_context_get_main_thread();
+
 class GjsContextPrivate : public JS::JobQueue {
  public:
     using DestroyNotify = void (*)(JSContext*, void* data);
@@ -114,6 +118,8 @@ class GjsContextPrivate : public JS::JobQueue {
 
     uint8_t m_exit_code;
 
+    void* m_worker;
+
     /* flags */
     std::atomic_bool m_destroying = ATOMIC_VAR_INIT(false);
     bool m_in_gc_sweep : 1;
@@ -168,6 +174,7 @@ class GjsContextPrivate : public JS::JobQueue {
     [[nodiscard]] static GjsContextPrivate* from_object(
         GjsContext* public_context);
     [[nodiscard]] static GjsContextPrivate* from_current_context();
+    [[nodiscard]] static GjsContextPrivate* from_main_thread_context();
 
     GjsContextPrivate(JSContext* cx, GjsContext* public_context);
     ~GjsContextPrivate(void);
@@ -194,6 +201,7 @@ class GjsContextPrivate : public JS::JobQueue {
     void set_search_path(char** value) { m_search_path = value; }
     void set_should_profile(bool value) { m_should_profile = value; }
     void set_execute_as_module(bool value) { m_exec_as_module = value; }
+    void set_worker(void* worker) { m_worker = worker; }
     void set_should_listen_sigusr2(bool value) {
         m_should_listen_sigusr2 = value;
     }
@@ -202,6 +210,11 @@ class GjsContextPrivate : public JS::JobQueue {
     [[nodiscard]] bool is_owner_thread() const {
         return m_owner_thread == std::this_thread::get_id();
     }
+    [[nodiscard]] bool is_main_thread() const {
+        static std::thread::id main_thread = std::this_thread::get_id();
+
+        return main_thread == std::this_thread::get_id();
+    }
     [[nodiscard]] JS::WeakCache<FundamentalTable>& fundamental_table() {
         return *m_fundamental_table;
     }
diff --git a/gjs/context.cpp b/gjs/context.cpp
index 7a6eba6e7..3d4894c65 100644
--- a/gjs/context.cpp
+++ b/gjs/context.cpp
@@ -88,6 +88,7 @@
 #include "gjs/profiler.h"
 #include "gjs/promise.h"
 #include "gjs/text-encoding.h"
+#include "gjs/thread.h"
 #include "modules/modules.h"
 #include "util/log.h"
 
@@ -139,7 +140,11 @@ GjsContextPrivate* GjsContextPrivate::from_object(GjsContext* js_context) {
 }
 
 GjsContextPrivate* GjsContextPrivate::from_current_context() {
-    return from_object(gjs_context_get_current());
+    return from_object(gjs_context_get_current_thread());
+}
+
+GjsContextPrivate* GjsContextPrivate::from_main_thread_context() {
+    return from_object(gjs_context_get_main_thread());
 }
 
 enum {
@@ -150,6 +155,7 @@ enum {
     PROP_PROFILER_ENABLED,
     PROP_PROFILER_SIGUSR2,
     PROP_EXEC_AS_MODULE,
+    PROP_WORKER_THREAD,
 };
 
 static GMutex contexts_lock;
@@ -226,6 +232,8 @@ setup_dump_heap(void)
     }
 }
 
+void gjs_context_set_thread_context(GjsContext* context);
+
 static void
 gjs_context_init(GjsContext *js_context)
 {
@@ -319,6 +327,13 @@ gjs_context_class_init(GjsContextClass *klass)
     g_object_class_install_property(object_class, PROP_EXEC_AS_MODULE, pspec);
     g_param_spec_unref(pspec);
 
+    pspec = g_param_spec_pointer(
+        "worker-thread", "The worker thread for this context",
+        "The worker thread for the context",
+        GParamFlags(G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY));
+    g_object_class_install_property(object_class, PROP_WORKER_THREAD, pspec);
+    g_param_spec_unref(pspec);
+
     /* For GjsPrivate */
     if (!g_getenv("GJS_USE_UNINSTALLED_FILES")) {
 #ifdef G_OS_WIN32
@@ -339,6 +354,7 @@ gjs_context_class_init(GjsContextClass *klass)
     gjs_register_native_module("_byteArrayNative", gjs_define_byte_array_stuff);
     gjs_register_native_module("_encodingNative",
                                gjs_define_text_encoding_stuff);
+    gjs_register_native_module("_workerNative", gjs_define_worker_stuff);
     gjs_register_native_module("_gi", gjs_define_private_gi_stuff);
     gjs_register_native_module("gi", gjs_define_repo);
 
@@ -485,8 +501,8 @@ GjsContextPrivate::~GjsContextPrivate(void) {
 static void
 gjs_context_finalize(GObject *object)
 {
-    if (gjs_context_get_current() == (GjsContext*)object)
-        gjs_context_make_current(NULL);
+    if (gjs_context_get_current_thread() == (GjsContext*)object)
+        gjs_context_make_current(nullptr);
 
     g_mutex_lock(&contexts_lock);
     all_contexts = g_list_remove(all_contexts, object);
@@ -578,7 +594,7 @@ static bool add_promise_reactions(JSContext* cx, JS::HandleValue promise,
 
 static void load_context_module(JSContext* cx, const char* uri,
                                 const char* debug_identifier) {
-    JS::RootedObject loader(cx, gjs_module_load(cx, uri, uri));
+    JS::RootedObject loader(cx, gjs_module_load(cx, uri, uri, true));
 
     if (!loader) {
         gjs_log_exception(cx);
@@ -599,8 +615,18 @@ static void load_context_module(JSContext* cx, const char* uri,
     GjsContextPrivate::from_cx(cx)->main_loop_hold();
     bool ok = add_promise_reactions(
         cx, evaluation_promise, on_context_module_resolved,
-        [](JSContext* cx, unsigned, JS::Value*) {
-            GjsContextPrivate::from_cx(cx)->main_loop_release();
+        [](JSContext* cx, unsigned argc, JS::Value* vp) {
+            JS::CallArgs args = JS::CallArgsFromVp(argc, vp);
+            JS::HandleValue error = args.get(0);
+
+            GjsContextPrivate* gjs_cx = GjsContextPrivate::from_cx(cx);
+            gjs_cx->report_unhandled_exception();
+
+            gjs_log_exception_full(cx, error, nullptr, G_LOG_LEVEL_CRITICAL);
+
+            gjs_cx->main_loop_release();
+
+            args.rval().setUndefined();
 
             // Abort because this module is required.
             g_error("Failed to load context module.");
@@ -678,8 +704,9 @@ GjsContextPrivate::GjsContextPrivate(JSContext* cx, GjsContext* public_context)
     }
 
     JS::RootedObject global(
-        m_cx,
-        gjs_create_global_object(cx, GjsGlobalType::DEFAULT, internal_global));
+        m_cx, gjs_create_global_object(
+                  cx, m_worker ? GjsGlobalType::WORKER : GjsGlobalType::DEFAULT,
+                  internal_global));
 
     if (!global) {
         gjs_log_exception(m_cx);
@@ -708,10 +735,24 @@ GjsContextPrivate::GjsContextPrivate(JSContext* cx, GjsContext* public_context)
         gjs_set_global_slot(global, GjsGlobalSlot::IMPORTS,
                             JS::ObjectValue(*importer));
 
-        if (!gjs_define_global_properties(m_cx, global, GjsGlobalType::DEFAULT,
-                                          "GJS", "default")) {
-            gjs_log_exception(m_cx);
-            g_error("Failed to define properties on global object");
+        if (!m_worker) {
+            [[maybe_unused]] bool main_thread = is_main_thread();
+            g_assert(main_thread);
+
+            if (!gjs_define_global_properties(
+                    m_cx, global, GjsGlobalType::DEFAULT, "GJS", "default")) {
+                gjs_log_exception(m_cx);
+                g_error("Failed to define properties on global object");
+            }
+        } else {
+            if (!gjs_define_global_properties(m_cx, global,
+                                              GjsGlobalType::WORKER, "GJS")) {
+                gjs_log_exception(m_cx);
+                g_error("Failed to define properties on worker global object");
+            }
+
+            gjs_set_global_slot(global, GjsWorkerGlobalSlot::WORKER,
+                                JS::PrivateValue(m_worker));
         }
     }
 
@@ -730,6 +771,7 @@ GjsContextPrivate::GjsContextPrivate(JSContext* cx, GjsContext* public_context)
         g_error("Failed to load internal module loaders.");
     }
 
+    // if (!m_worker)
     {
         JSAutoRealm ar(cx, global);
         load_context_module(
@@ -799,6 +841,9 @@ gjs_context_set_property (GObject      *object,
     case PROP_EXEC_AS_MODULE:
         gjs->set_execute_as_module(g_value_get_boolean(value));
         break;
+    case PROP_WORKER_THREAD:
+        gjs->set_worker(g_value_get_pointer(value));
+        break;
     default:
         G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
         break;
@@ -820,6 +865,11 @@ gjs_context_new_with_search_path(char** search_path)
                          NULL);
 }
 
+GjsContext* gjs_context_new_worker() {
+    return static_cast<GjsContext*>(
+        g_object_new(GJS_TYPE_CONTEXT, "worker-thread", true, nullptr));
+}
+
 gboolean GjsContextPrivate::trigger_gc_if_needed(void* data) {
     auto* gjs = static_cast<GjsContextPrivate*>(data);
     gjs->m_auto_gc_id = 0;
@@ -1654,20 +1704,44 @@ void gjs_context_set_argv(GjsContext* js_context, ssize_t array_length,
     gjs->set_args(std::move(args));
 }
 
-static GjsContext *current_context;
+static GjsContext* main_thread_context = nullptr;
+static thread_local GjsContext* current_thread_context = nullptr;
 
-GjsContext *
-gjs_context_get_current (void)
-{
-    return current_context;
+GjsContext* gjs_context_get_main_thread(void) { return main_thread_context; }
+
+[[deprecated]] GjsContext* gjs_context_get_current() {
+    return gjs_context_get_main_thread();
+}
+
+GjsContext* gjs_context_get_current_thread(void) {
+    return current_thread_context;
+}
+
+void gjs_context_set_current_thread(GjsContext* context) {
+    g_assert(context == NULL || current_thread_context == NULL);
+
+    current_thread_context = context;
+}
+
+void gjs_context_set_main_thread(GjsContext* context) {
+    g_assert(context == NULL || main_thread_context == NULL);
+
+    main_thread_context = context;
 }
 
 void
 gjs_context_make_current (GjsContext *context)
 {
-    g_assert (context == NULL || current_context == NULL);
+    static std::thread::id parent_thread = std::this_thread::get_id();
+
+    if (std::this_thread::get_id() == parent_thread) {
+        g_assert(context == nullptr || main_thread_context == nullptr);
+        // Only make this context the parent current if this is the main thread.
+        gjs_context_set_main_thread(context);
+    }
 
-    current_context = context;
+    g_assert(context == nullptr || current_thread_context == nullptr);
+    gjs_context_set_current_thread(context);
 }
 
 /**
diff --git a/gjs/coverage.cpp b/gjs/coverage.cpp
index 7dcc20db3..3c3cca525 100644
--- a/gjs/coverage.cpp
+++ b/gjs/coverage.cpp
@@ -31,8 +31,6 @@
 #include "gjs/jsapi-util.h"
 #include "gjs/macros.h"
 
-static bool s_coverage_enabled = false;
-
 struct _GjsCoverage {
     GObject parent;
 };
@@ -178,9 +176,18 @@ write_line(GOutputStream *out,
     return g_output_stream_printf(out, nullptr, nullptr, error, "%s\n", line);
 }
 
+static bool coverage_is_enabled(bool enable_coverage = false) {
+    static bool s_coverage_enabled = false;
+
+    if (enable_coverage)
+        s_coverage_enabled = true;
+
+    return s_coverage_enabled;
+}
+
 [[nodiscard]] static GjsAutoUnref<GFile> write_statistics_internal(
     GjsCoverage* coverage, JSContext* cx, GError** error) {
-    if (!s_coverage_enabled) {
+    if (!coverage_is_enabled()) {
         g_critical(
             "Code coverage requested, but gjs_coverage_enable() was not called."
             " You must call this function before creating any GjsContext.");
@@ -296,7 +303,7 @@ gjs_coverage_write_statistics(GjsCoverage *coverage)
 }
 
 static void gjs_coverage_init(GjsCoverage*) {
-    if (!s_coverage_enabled)
+    if (!coverage_is_enabled())
         g_critical(
             "Code coverage requested, but gjs_coverage_enable() was not called."
             " You must call this function before creating any GjsContext.");
@@ -497,5 +504,5 @@ gjs_coverage_new (const char * const *prefixes,
  */
 void gjs_coverage_enable() {
     js::EnableCodeCoverage();
-    s_coverage_enabled = true;
+    coverage_is_enabled(true);
 }
diff --git a/gjs/engine.cpp b/gjs/engine.cpp
index 16d84cc19..9f32c8df1 100644
--- a/gjs/engine.cpp
+++ b/gjs/engine.cpp
@@ -79,7 +79,7 @@ class GjsSourceHook : public js::SourceHook {
 
 #ifdef G_OS_WIN32
 HMODULE gjs_dll;
-static bool gjs_is_inited = false;
+static bool _gjs_is_inited = false;
 
 BOOL WINAPI
 DllMain (HINSTANCE hinstDLL,
@@ -90,7 +90,7 @@ LPVOID    lpvReserved)
   {
   case DLL_PROCESS_ATTACH:
     gjs_dll = hinstDLL;
-    gjs_is_inited = JS_Init();
+    _gjs_is_inited = JS_Init();
     break;
 
   case DLL_THREAD_DETACH:
@@ -105,6 +105,7 @@ LPVOID    lpvReserved)
   return TRUE;
 }
 
+static bool gjs_is_inited() { return _gjs_is_inited; }
 #else
 class GjsInit {
 public:
@@ -120,12 +121,28 @@ public:
     explicit operator bool() const { return true; }
 };
 
-static GjsInit gjs_is_inited;
+static bool gjs_is_inited() {
+    static GjsInit gjs_is_inited;
+
+    return !!gjs_is_inited;
+}
 #endif
 
 JSContext* gjs_create_js_context(GjsContextPrivate* uninitialized_gjs) {
-    g_assert(gjs_is_inited);
-    JSContext *cx = JS_NewContext(32 * 1024 * 1024 /* max bytes */);
+    g_assert(gjs_is_inited());
+
+    // Store the first runtime as the "parent" runtime, all future
+    // contexts will inherit from it.
+    static std::thread::id parent_thread = std::this_thread::get_id();
+    static JSRuntime* parent_runtime = nullptr;
+
+    JSContext* cx = JS_NewContext(32 * 1024 * 1024 /* max bytes */,
+                                  (parent_thread != std::this_thread::get_id())
+                                      ? parent_runtime
+                                      : nullptr);
+    if (!parent_runtime) {
+        parent_runtime = JS_GetRuntime(cx);
+    }
     if (!cx)
         return nullptr;
 
diff --git a/gjs/global.cpp b/gjs/global.cpp
index 3ed6ac5f3..b88ea7810 100644
--- a/gjs/global.cpp
+++ b/gjs/global.cpp
@@ -36,6 +36,7 @@
 #include "gjs/internal.h"
 #include "gjs/jsapi-util.h"
 #include "gjs/native.h"
+#include "gjs/thread.h"
 
 namespace mozilla {
 union Utf8Unit;
@@ -70,6 +71,27 @@ class GjsBaseGlobal {
     }
 
  protected:
+    GJS_JSAPI_RETURN_CONVENTION
+    static bool define_registry_properties(JSContext* cx,
+                                           JS::HandleObject global) {
+        JSAutoRealm ar(cx, global);
+
+        JS::RootedObject native_registry(cx, JS::NewMapObject(cx));
+        if (!native_registry)
+            return false;
+
+        gjs_set_global_slot(global, GjsGlobalSlot::NATIVE_REGISTRY,
+                            JS::ObjectValue(*native_registry));
+
+        JS::RootedObject module_registry(cx, JS::NewMapObject(cx));
+        if (!module_registry)
+            return false;
+
+        gjs_set_global_slot(global, GjsGlobalSlot::MODULE_REGISTRY,
+                            JS::ObjectValue(*module_registry));
+        return true;
+    }
+
     [[nodiscard]] static JSObject* create(
         JSContext* cx, const JSClass* clasp,
         JS::RealmCreationOptions options = JS::RealmCreationOptions()) {
@@ -84,11 +106,19 @@ class GjsBaseGlobal {
         return base(cx, clasp, options);
     }
 
+    static void define_realm_name(JSContext*, JS::HandleObject global,
+                                  const char* realm_name) {
+        JS::Realm* realm = JS::GetObjectRealmOrNull(global);
+        g_assert(realm && "Global object must be associated with a realm");
+        // const_cast is allowed here if we never free the realm data
+        JS::SetRealmPrivate(realm, const_cast<char*>(realm_name));
+    }
+
     GJS_JSAPI_RETURN_CONVENTION
     static bool run_bootstrap(JSContext* cx, const char* bootstrap_script,
                               JS::HandleObject global) {
         GjsAutoChar uri = g_strdup_printf(
-            "resource:///org/gnome/gjs/modules/script/_bootstrap/%s.js",
+            "resource:///org/gnome/gjs/modules/_bootstrap/%s.js",
             bootstrap_script);
 
         JSAutoRealm ar(cx, global);
@@ -144,7 +174,7 @@ class GjsBaseGlobal {
 
 const JSClassOps defaultclassops = JS::DefaultGlobalClassOps;
 
-class GjsGlobal : GjsBaseGlobal {
+class GjsGlobal : public GjsBaseGlobal {
     static constexpr JSClass klass = {
         // Jasmine depends on the class name "GjsGlobal" to detect GJS' global
         // object.
@@ -160,8 +190,7 @@ class GjsGlobal : GjsBaseGlobal {
         JS_PS_END};
     // clang-format on
 
-    static constexpr JSFunctionSpec static_funcs[] = {
-        JS_FS_END};
+    static constexpr JSFunctionSpec static_funcs[] = {JS_FS_END};
 
  public:
     [[nodiscard]] static JSObject* create(JSContext* cx) {
@@ -184,29 +213,15 @@ class GjsGlobal : GjsBaseGlobal {
             !JS_DefineProperties(cx, global, GjsGlobal::static_props))
             return false;
 
-        JS::Realm* realm = JS::GetObjectRealmOrNull(global);
-        g_assert(realm && "Global object must be associated with a realm");
-        // const_cast is allowed here if we never free the realm data
-        JS::SetRealmPrivate(realm, const_cast<char*>(realm_name));
+        define_realm_name(cx, global, realm_name);
 
-        JS::RootedObject native_registry(cx, JS::NewMapObject(cx));
-        if (!native_registry)
+        if (!define_registry_properties(cx, global))
             return false;
 
-        gjs_set_global_slot(global, GjsGlobalSlot::NATIVE_REGISTRY,
-                            JS::ObjectValue(*native_registry));
-
-        JS::RootedObject module_registry(cx, JS::NewMapObject(cx));
-        if (!module_registry)
-            return false;
-
-        gjs_set_global_slot(global, GjsGlobalSlot::MODULE_REGISTRY,
-                            JS::ObjectValue(*module_registry));
-
         JS::Value v_importer =
             gjs_get_global_slot(global, GjsGlobalSlot::IMPORTS);
-        g_assert(((void) "importer should be defined before passing null "
-                  "importer to GjsGlobal::define_properties",
+        g_assert(((void)"importer should be defined before passing null "
+                        "importer to GjsGlobal::define_properties",
                   v_importer.isObject()));
         JS::RootedObject root_importer(cx, &v_importer.toObject());
 
@@ -225,6 +240,56 @@ class GjsGlobal : GjsBaseGlobal {
     }
 };
 
+class GjsWorkerGlobal : public GjsGlobal {
+    static constexpr JSClass klass = {
+        "GjsWorkerGlobal",
+        JSCLASS_GLOBAL_FLAGS_WITH_SLOTS(
+            static_cast<uint32_t>(GjsWorkerGlobalSlot::LAST)),
+        &defaultclassops,
+    };
+
+    // clang-format off
+    static constexpr JSPropertySpec static_props[] = {
+        JS_STRING_SYM_PS(toStringTag, "GjsWorkerGlobal", JSPROP_READONLY),
+        JS_PS_END};
+    // clang-format on
+
+    static constexpr JSFunctionSpec static_funcs[] = {
+        JS_FN("postMessage", Gjs::WorkerGlobal::post_message, 2, 0), JS_FS_END};
+
+ public:
+    [[nodiscard]] static JSObject* create(JSContext* cx) {
+        return GjsBaseGlobal::create(cx, &klass);
+    }
+
+    [[nodiscard]] static JSObject* create_with_compartment(
+        JSContext* cx, JS::HandleObject cmp_global) {
+        return GjsBaseGlobal::create_with_compartment(cx, cmp_global, &klass);
+    }
+
+    GJS_JSAPI_RETURN_CONVENTION
+    static bool define_properties(JSContext* cx, JS::HandleObject global,
+                                  const char* realm_name) {
+        const GjsAtoms& atoms = GjsContextPrivate::atoms(cx);
+        if (!JS_DefineProperty(
+                cx, global, "self", global,
+                JSPROP_ENUMERATE | JSPROP_READONLY | JSPROP_PERMANENT) ||
+            !JS_DefineFunctions(cx, global, GjsWorkerGlobal::static_funcs) ||
+            !JS_DefineProperties(cx, global, GjsWorkerGlobal::static_props))
+            return false;
+
+        define_realm_name(cx, global, realm_name);
+
+        if (!define_registry_properties(cx, global))
+            return false;
+
+        if (!run_bootstrap(cx, "worker", global))
+            return false;
+
+        return true;
+    }
+};
+
 class GjsDebuggerGlobal : GjsBaseGlobal {
     static constexpr JSClass klass = {
         "GjsDebuggerGlobal",
@@ -257,10 +322,7 @@ class GjsDebuggerGlobal : GjsBaseGlobal {
             !JS_DefineFunctions(cx, global, GjsDebuggerGlobal::static_funcs))
             return false;
 
-        JS::Realm* realm = JS::GetObjectRealmOrNull(global);
-        g_assert(realm && "Global object must be associated with a realm");
-        // const_cast is allowed here if we never free the realm data
-        JS::SetRealmPrivate(realm, const_cast<char*>(realm_name));
+        define_realm_name(cx, global, realm_name);
 
         if (bootstrap_script) {
             if (!run_bootstrap(cx, bootstrap_script, global))
@@ -271,7 +333,7 @@ class GjsDebuggerGlobal : GjsBaseGlobal {
     }
 };
 
-class GjsInternalGlobal : GjsBaseGlobal {
+class GjsInternalGlobal : public GjsBaseGlobal {
     static constexpr JSFunctionSpec static_funcs[] = {
         JS_FN("compileModule", gjs_internal_compile_module, 2, 0),
         JS_FN("compileInternalModule", gjs_internal_compile_internal_module, 2,
@@ -287,6 +349,7 @@ class GjsInternalGlobal : GjsBaseGlobal {
               0),
         JS_FN("setModulePrivate", gjs_internal_set_module_private, 2, 0),
         JS_FN("uriExists", gjs_internal_uri_exists, 1, 0),
+        JS_FN("loadNative", &load_native_module, 1, 0),
         JS_FS_END};
 
     static constexpr JSClass klass = {
@@ -310,26 +373,13 @@ class GjsInternalGlobal : GjsBaseGlobal {
                                   const char* realm_name,
                                   const char* bootstrap_script
                                   [[maybe_unused]]) {
-        JS::Realm* realm = JS::GetObjectRealmOrNull(global);
-        g_assert(realm && "Global object must be associated with a realm");
-        // const_cast is allowed here if we never free the realm data
-        JS::SetRealmPrivate(realm, const_cast<char*>(realm_name));
+        define_realm_name(cx, global, realm_name);
 
         JSAutoRealm ar(cx, global);
-        JS::RootedObject native_registry(cx, JS::NewMapObject(cx));
-        if (!native_registry)
-            return false;
-
-        gjs_set_global_slot(global, GjsGlobalSlot::NATIVE_REGISTRY,
-                            JS::ObjectValue(*native_registry));
 
-        JS::RootedObject module_registry(cx, JS::NewMapObject(cx));
-        if (!module_registry)
+        if (!define_registry_properties(cx, global))
             return false;
 
-        gjs_set_global_slot(global, GjsGlobalSlot::MODULE_REGISTRY,
-                            JS::ObjectValue(*module_registry));
-
         return JS_DefineFunctions(cx, global, static_funcs);
     }
 };
@@ -349,6 +399,9 @@ JSObject* gjs_create_global_object(JSContext* cx, GjsGlobalType global_type,
         switch (global_type) {
             case GjsGlobalType::DEFAULT:
                 return GjsGlobal::create_with_compartment(cx, current_global);
+            case GjsGlobalType::WORKER:
+                return GjsWorkerGlobal::create_with_compartment(cx,
+                                                                current_global);
             case GjsGlobalType::DEBUGGER:
                 return GjsDebuggerGlobal::create_with_compartment(
                     cx, current_global);
@@ -363,6 +416,9 @@ JSObject* gjs_create_global_object(JSContext* cx, GjsGlobalType global_type,
     switch (global_type) {
         case GjsGlobalType::DEFAULT:
             return GjsGlobal::create(cx);
+        case GjsGlobalType::WORKER:
+            // A worker global cannot be created without an existing global
+            g_assert_not_reached();
         case GjsGlobalType::DEBUGGER:
             return GjsDebuggerGlobal::create(cx);
         case GjsGlobalType::INTERNAL:
@@ -485,7 +541,7 @@ bool gjs_global_registry_get(JSContext* cx, JS::HandleObject registry,
  * @global: a JS global object that has not yet been passed to this function
  * @realm_name: (nullable): name of the realm, for debug output
  * @bootstrap_script: (nullable): name of a bootstrap script (found at
- * resource://org/gnome/gjs/modules/script/_bootstrap/@bootstrap_script) or
+ * resource://org/gnome/gjs/modules/_bootstrap/@bootstrap_script) or
  * %NULL for none
  *
  * Defines properties on the global object such as 'window' and 'imports', and
@@ -516,6 +572,8 @@ bool gjs_define_global_properties(JSContext* cx, JS::HandleObject global,
         case GjsGlobalType::DEFAULT:
             return GjsGlobal::define_properties(cx, global, realm_name,
                                                 bootstrap_script);
+        case GjsGlobalType::WORKER:
+            return GjsWorkerGlobal::define_properties(cx, global, realm_name);
         case GjsGlobalType::DEBUGGER:
             return GjsDebuggerGlobal::define_properties(cx, global, realm_name,
                                                         bootstrap_script);
@@ -540,10 +598,14 @@ decltype(GjsGlobal::klass) constexpr GjsGlobal::klass;
 decltype(GjsGlobal::static_funcs) constexpr GjsGlobal::static_funcs;
 decltype(GjsGlobal::static_props) constexpr GjsGlobal::static_props;
 
+decltype(GjsWorkerGlobal::klass) constexpr GjsWorkerGlobal::klass;
+decltype(GjsWorkerGlobal::static_funcs) constexpr GjsWorkerGlobal::static_funcs;
+decltype(GjsWorkerGlobal::static_props) constexpr GjsWorkerGlobal::static_props;
+
 decltype(GjsDebuggerGlobal::klass) constexpr GjsDebuggerGlobal::klass;
-decltype(
-    GjsDebuggerGlobal::static_funcs) constexpr GjsDebuggerGlobal::static_funcs;
+decltype(GjsDebuggerGlobal::static_funcs) constexpr GjsDebuggerGlobal::
+    static_funcs;
 
 decltype(GjsInternalGlobal::klass) constexpr GjsInternalGlobal::klass;
-decltype(
-    GjsInternalGlobal::static_funcs) constexpr GjsInternalGlobal::static_funcs;
+decltype(GjsInternalGlobal::static_funcs) constexpr GjsInternalGlobal::
+    static_funcs;
diff --git a/gjs/global.h b/gjs/global.h
index 569a8ce16..593244060 100644
--- a/gjs/global.h
+++ b/gjs/global.h
@@ -24,6 +24,7 @@ enum class GjsGlobalType {
     DEFAULT,
     DEBUGGER,
     INTERNAL,
+    WORKER,
 };
 
 enum class GjsBaseGlobalSlot : uint32_t {
@@ -60,6 +61,7 @@ enum class GjsGlobalSlot : uint32_t {
     PROTOTYPE_cairo_surface,
     PROTOTYPE_cairo_surface_pattern,
     PROTOTYPE_cairo_svg_surface,
+    PROTOTYPE_worker,
     LAST,
 };
 
@@ -67,6 +69,13 @@ enum class GjsInternalGlobalSlot : uint32_t {
     LAST = static_cast<uint32_t>(GjsGlobalSlot::LAST),
 };
 
+enum class GjsWorkerGlobalSlot : uint32_t {
+    WORKER = static_cast<uint32_t>(GjsGlobalSlot::LAST),
+    ONMESSAGE,
+    ONERROR,
+    LAST,
+};
+
 bool gjs_global_is_type(JSContext* cx, GjsGlobalType type);
 GjsGlobalType gjs_global_get_type(JSContext* cx);
 GjsGlobalType gjs_global_get_type(JSObject* global);
@@ -87,7 +96,7 @@ GJS_JSAPI_RETURN_CONVENTION
 bool gjs_define_global_properties(JSContext* cx, JS::HandleObject global,
                                   GjsGlobalType global_type,
                                   const char* realm_name,
-                                  const char* bootstrap_script);
+                                  const char* bootstrap_script = nullptr);
 
 namespace detail {
 void set_global_slot(JSObject* global, uint32_t slot, JS::Value value);
@@ -99,6 +108,7 @@ inline void gjs_set_global_slot(JSObject* global, Slot slot, JS::Value value) {
     static_assert(std::is_same_v<GjsBaseGlobalSlot, Slot> ||
                       std::is_same_v<GjsGlobalSlot, Slot> ||
                       std::is_same_v<GjsInternalGlobalSlot, Slot> ||
+                      std::is_same_v<GjsWorkerGlobalSlot, Slot> ||
                       std::is_same_v<GjsDebuggerGlobalSlot, Slot>,
                   "Must use a GJS global slot enum");
     detail::set_global_slot(global, static_cast<uint32_t>(slot), value);
@@ -109,6 +119,7 @@ inline JS::Value gjs_get_global_slot(JSObject* global, Slot slot) {
     static_assert(std::is_same_v<GjsBaseGlobalSlot, Slot> ||
                       std::is_same_v<GjsGlobalSlot, Slot> ||
                       std::is_same_v<GjsInternalGlobalSlot, Slot> ||
+                      std::is_same_v<GjsWorkerGlobalSlot, Slot> ||
                       std::is_same_v<GjsDebuggerGlobalSlot, Slot>,
                   "Must use a GJS global slot enum");
     return detail::get_global_slot(global, static_cast<uint32_t>(slot));
diff --git a/gjs/native.cpp b/gjs/native.cpp
index 1bdea30ee..35916adc1 100644
--- a/gjs/native.cpp
+++ b/gjs/native.cpp
@@ -4,6 +4,7 @@
 
 #include <config.h>
 
+#include <mutex>
 #include <string>
 #include <tuple>  // for tie
 #include <unordered_map>
@@ -19,11 +20,12 @@
 #include "util/log.h"
 
 static std::unordered_map<std::string, GjsDefineModuleFunc> modules;
-
+static std::mutex modules_mutex;
 void
 gjs_register_native_module (const char          *module_id,
                             GjsDefineModuleFunc  func)
 {
+    std::lock_guard<std::mutex> lock(modules_mutex);
     bool inserted;
     std::tie(std::ignore, inserted) = modules.insert({module_id, func});
     if (!inserted) {
@@ -32,8 +34,7 @@ gjs_register_native_module (const char          *module_id,
         return;
     }
 
-    gjs_debug(GJS_DEBUG_NATIVE,
-              "Registered native JS module '%s'",
+    gjs_debug(GJS_DEBUG_NATIVE, "Registered native JS module '%s'\n",
               module_id);
 }
 
@@ -46,6 +47,7 @@ gjs_register_native_module (const char          *module_id,
  * builtin module without starting to try and load it.
  */
 bool gjs_is_registered_native_module(const char* name) {
+    std::lock_guard<std::mutex> lock(modules_mutex);
     return modules.count(name) > 0;
 }
 
@@ -66,9 +68,9 @@ gjs_load_native_module(JSContext              *context,
                        const char             *parse_name,
                        JS::MutableHandleObject module_out)
 {
-    gjs_debug(GJS_DEBUG_NATIVE,
-              "Defining native module '%s'",
-              parse_name);
+    std::lock_guard<std::mutex> lock(modules_mutex);
+
+    gjs_debug(GJS_DEBUG_NATIVE, "Defining native module '%s'\n", parse_name);
 
     const auto& iter = modules.find(parse_name);
 
diff --git a/gjs/thread.cpp b/gjs/thread.cpp
new file mode 100644
index 000000000..b2731e1e4
--- /dev/null
+++ b/gjs/thread.cpp
@@ -0,0 +1,447 @@
+/* -*- mode: C++; c-basic-offset: 4; indent-tabs-mode: nil; -*- */
+// SPDX-License-Identifier: MIT OR LGPL-2.0-or-later
+// SPDX-FileCopyrightText: 2009 Red Hat, Inc.
+
+#include <config.h>
+
+#include <iostream>
+#include <mutex>
+#include <thread>
+
+#include <glib-object.h>
+#include <glib.h>
+
+#include <js/RootingAPI.h>
+#include <js/TypeDecls.h>
+#include <jsfriendapi.h>  // for DumpBacktrace
+
+#include <js/ContextOptions.h>
+#include <js/StructuredClone.h>
+#include <js/Vector.h>
+#include <jsapi.h>
+#include <jsfriendapi.h>
+
+#include "gi/cwrapper.h"
+#include "gjs/context-private.h"
+#include "gjs/context.h"
+#include "gjs/global.h"
+#include "gjs/jsapi-util-args.h"
+#include "gjs/jsapi-util.h"
+#include "gjs/thread.h"
+
+#include <algorithm>
+#include <functional>
+#include <tuple>
+
+namespace Gjs {
+class NativeWorker;
+
+static int worker_count = 0;
+static std::mutex workerThreadsLock;
+static std::vector<NativeWorker*> workerThreads;
+
+class NativeWorkerOptions {
+    friend NativeWorker;
+
+    GjsAutoChar m_uri;
+    GjsAutoChar m_name;
+
+    explicit NativeWorkerOptions(const char* uri, const char* name)
+        : m_uri(const_cast<char*>(uri), GjsAutoTakeOwnership()),
+          m_name(const_cast<char*>(name), GjsAutoTakeOwnership()) {}
+};
+
+NativeWorker* GetCurrentThreadWorkerPrivate(JSContext* cx) {
+    GjsContextPrivate* gjs = GjsContextPrivate::from_cx(cx);
+    if (!gjs || gjs->is_main_thread()) {
+        return nullptr;
+    }
+
+    JS::RootedValue workerPrivate(
+        gjs->context(),
+        gjs_get_global_slot(gjs->global(), GjsWorkerGlobalSlot::WORKER));
+
+    if (!workerPrivate.isDouble())
+        return nullptr;
+
+    NativeWorker* worker =
+        static_cast<NativeWorker*>(workerPrivate.toPrivate());
+
+    if (!worker) {
+        return nullptr;
+    }
+
+    return worker;
+}
+
+class NativeWorker : public CWrapper<NativeWorker> {
+    friend CWrapperPointerOps<NativeWorker>;
+    friend CWrapper<NativeWorker>;
+    friend NativeWorkerOptions;
+
+    GThread* m_thread;
+    NativeWorkerOptions m_options;
+
+    GMainContext* m_parent_main_context;
+    GMainContext* m_main_context;
+
+    std::unique_ptr<JSAutoStructuredCloneBuffer> m_buffer;
+    std::unique_ptr<JSAutoStructuredCloneBuffer> m_host_buffer;
+
+    JS::Heap<JSFunction*> m_received;
+
+    static void* NativeWorkerMain(NativeWorker* worker) {
+        // Set the main context for this thread...
+        g_main_context_push_thread_default(worker->m_main_context);
+
+        GjsContext* object = gjs_context_new_worker();
+        GjsContextPrivate* gjs = GjsContextPrivate::from_object(object);
+        gjs_set_global_slot(gjs->global(), GjsWorkerGlobalSlot::WORKER,
+                            JS::PrivateValue(worker));
+        JSContext* cx = gjs->context();
+        JSAutoRealm ar(cx, gjs->global());
+
+        GError* error;
+        char* str = g_strdup(worker->m_options.m_uri);
+
+        if (!gjs->register_module(str, str, &error)) {
+            gjs_log_exception(cx);
+            return nullptr;
+        }
+
+        if (!gjs->eval_module(str, nullptr, &error)) {
+            gjs_log_exception(cx);
+            return nullptr;
+        }
+
+        GMainLoop* ml = g_main_loop_new(worker->m_main_context, false);
+        g_main_loop_run(ml);
+
+        return nullptr;
+    }
+
+    NativeWorker(NativeWorker&) = delete;
+    NativeWorker(NativeWorker&&) = delete;
+
+    static constexpr GjsGlobalSlot PROTOTYPE_SLOT =
+        GjsGlobalSlot::PROTOTYPE_worker;
+    static constexpr GjsDebugTopic DEBUG_TOPIC = GJS_DEBUG_CONTEXT;
+    static constexpr unsigned constructor_nargs = 1;
+
+    NativeWorker(NativeWorkerOptions& options)
+        : m_thread(nullptr),
+          m_options(options),
+          m_parent_main_context(g_main_context_ref_thread_default()),
+          m_main_context(g_main_context_new()),
+          m_buffer(std::make_unique<JSAutoStructuredCloneBuffer>(
+              JS::StructuredCloneScope::SameProcess, nullptr, this)),
+          m_host_buffer(std::make_unique<JSAutoStructuredCloneBuffer>(
+              JS::StructuredCloneScope::SameProcess, nullptr, this)){};
+
+    ~NativeWorker() {
+        g_thread_unref(m_thread);
+        g_main_context_unref(m_parent_main_context);
+        g_main_context_pop_thread_default(m_main_context);
+    }
+
+    void run() {
+        workerThreadsLock.lock();
+        GjsAutoChar name(g_strdup_printf("NativeWorker %i", worker_count++));
+        workerThreadsLock.unlock();
+
+        m_thread = g_thread_new(
+            name.get(), reinterpret_cast<GThreadFunc>(&NativeWorkerMain), this);
+    }
+
+ public:
+    bool writeToHost(JSContext* cx, JS::HandleValue write) {
+        bool ok = m_host_buffer->write(cx, write, nullptr, this);
+        if (!ok)
+            return false;
+
+        GSource* source = g_idle_source_new();
+
+        g_source_set_callback(
+            source,
+            [](void* user_data) -> int {
+                NativeWorker* worker = static_cast<NativeWorker*>(user_data);
+                if (worker->m_received) {
+                    GjsContextPrivate* gjs =
+                        GjsContextPrivate::from_current_context();
+                    JSContext* cx = gjs->context();
+                    JSAutoRealm ar(cx, gjs->global());
+
+                    JS::CloneDataPolicy policy;
+                    policy.allowSharedMemoryObjects();
+                    JS::RootedValue read(cx);
+
+                    if (!worker->m_host_buffer->read(cx, &read, policy, nullptr,
+                                                     worker))
+                        return false;
+                    JS::RootedFunction fn(cx, worker->m_received);
+
+                    JS::RootedValueArray<1> args(cx);
+                    args[0].set(read);
+                    JS::RootedValue ignored_rval(cx);
+                    if (!JS_CallFunction(cx, nullptr, fn, args,
+                                         &ignored_rval)) {
+                        gjs_log_exception(cx);
+                        return false;
+                    }
+
+                    return false;
+                }
+
+                return false;
+            },
+            this, nullptr);
+
+        g_source_attach(source, m_parent_main_context);
+        return true;
+    }
+
+ private:
+    bool writeToWorker(JSContext* cx, JS::HandleValue write) {
+        bool ok = m_buffer->write(cx, write, nullptr, this);
+        if (!ok)
+            return false;
+
+        GSource* source = g_idle_source_new();
+        g_source_set_callback(
+            source,
+            [](void* user_data) -> int {
+                NativeWorker* worker = static_cast<NativeWorker*>(user_data);
+                worker->receive();
+
+                return false;
+            },
+            this, nullptr);
+
+        g_source_attach(source, m_main_context);
+        return true;
+    }
+
+    bool receive() {
+        GjsContextPrivate* gjs = GjsContextPrivate::from_current_context();
+        JSContext* cx = gjs->context();
+        JS::RootedObject global(cx, gjs->global());
+        JSAutoRealm ar(cx, global);
+
+        JS::CloneDataPolicy policy;
+        policy.allowSharedMemoryObjects();
+        JS::RootedValue read(cx);
+
+        if (!m_buffer->read(cx, &read, policy, nullptr, this))
+            return false;
+
+        JS::RootedString str(cx, JS::ToString(cx, read));
+        JS::UniqueChars c = JS_EncodeStringToUTF8(cx, str);
+
+        JS::RootedValue v_onmessage(
+            cx, gjs_get_global_slot(global, GjsWorkerGlobalSlot::ONMESSAGE));
+        if (!v_onmessage.isUndefined()) {
+            JS::RootedValueArray<1> args(cx);
+            args[0].set(read);
+            JS::RootedValue ignored_rval(cx);
+            if (!JS_CallFunctionValue(cx, nullptr, v_onmessage, args,
+                                      &ignored_rval)) {
+                gjs_log_exception(cx);
+                return false;
+            }
+        }
+        return true;
+    }
+
+    GJS_JSAPI_RETURN_CONVENTION
+    static NativeWorker* constructor_impl(JSContext* cx,
+                                          const JS::CallArgs& args) {
+        JS::UniqueChars specifier, name;
+        if (!gjs_parse_call_args(cx, "NativeWorker", args, "s?s", "uri",
+                                 &specifier, "name", &name))
+            return nullptr;
+
+        GjsContextPrivate::from_cx(cx)->main_loop_hold();
+        GjsAutoUnref<GFile> file(
+            g_file_new_for_commandline_arg(specifier.get()));
+        GjsAutoChar uri(g_file_get_uri(file));
+
+        workerThreadsLock.lock();
+        GjsAutoChar resolved_name(
+            name ? g_strdup_printf("GJS Worker %s", name.get())
+                 : g_strdup_printf("GJS Worker %i", worker_count++));
+        workerThreadsLock.unlock();
+
+        NativeWorkerOptions options(uri, name.get());
+
+        auto* worker = new NativeWorker(options);
+
+        workerThreadsLock.lock();
+        workerThreads.push_back(worker);
+        workerThreadsLock.unlock();
+
+        worker->run();
+
+        return worker;
+    }
+
+    static bool write(JSContext* cx, unsigned argc, JS::Value* vp) {
+        JS::CallArgs args = JS::CallArgsFromVp(argc, vp);
+
+        if (!args.requireAtLeast(cx, "write", 1))
+            return false;
+
+        JS::RootedObject wrapper(cx);
+        if (!args.computeThis(cx, &wrapper))
+            return false;
+        JS::RootedValue value(cx, args[0]);
+        NativeWorker* worker;
+        if (!NativeWorker::for_js_typecheck(cx, wrapper, &worker))
+            return false;
+
+        return worker->writeToWorker(cx, value);
+    }
+
+    static bool set_host_receiver(JSContext* cx, unsigned argc, JS::Value* vp) {
+        JS::CallArgs args = JS::CallArgsFromVp(argc, vp);
+
+        JS::RootedObject object(cx);
+        if (!gjs_parse_call_args(cx, "set_host_receiver", args, "o", "object",
+                                 &object))
+            return false;
+
+        JS::RootedObject wrapper(cx);
+        if (!args.computeThis(cx, &wrapper))
+            return false;
+
+        NativeWorker* worker;
+        if (!NativeWorker::for_js_typecheck(cx, wrapper, &worker))
+            return false;
+
+        if (!JS_ObjectIsFunction(object))
+            return false;
+
+        worker->m_received = JS_GetObjectFunction(object);
+        return true;
+    }
+
+    static void finalize_impl(JSFreeOp*, NativeWorker* thread) {
+        thread->~NativeWorker();
+    }
+
+    static void trace(JSTracer* tracer, JSObject* object) {
+        NativeWorker* priv = NativeWorker::for_js_nocheck(object);
+
+        JS::TraceEdge<JSFunction*>(tracer, &priv->m_received,
+                                   "NativeWorker::m_received");
+    }
+
+    static constexpr JSPropertySpec proto_props[] = {JS_PS_END};
+
+    static constexpr JSFunctionSpec proto_funcs[] = {
+        JS_FN("write", &NativeWorker::write, 1, 0),
+        JS_FN("setReceiver", &NativeWorker::set_host_receiver, 1, 0),
+        JS_FS_END};
+
+    static constexpr js::ClassSpec class_spec = {
+        nullptr,  // createConstructor
+        nullptr,  // createPrototype
+        nullptr,  // constructorFunctions
+        nullptr,  // constructorProperties
+        NativeWorker::proto_funcs,
+        NativeWorker::proto_props,
+        nullptr,  // define_gtype_prop
+    };
+    static constexpr struct JSClassOps class_ops = {
+        nullptr,
+        nullptr,  // deleteProperty
+        nullptr,  // enumerate
+        nullptr,
+        nullptr,
+        nullptr,  // mayResolve
+        &NativeWorker::finalize,
+        NULL,
+        NULL,
+        NULL,
+        &NativeWorker::trace,
+    };
+
+    static constexpr JSClass klass = {
+        "NativeWorker", JSCLASS_HAS_PRIVATE | JSCLASS_BACKGROUND_FINALIZE,
+        &NativeWorker::class_ops, &NativeWorker::class_spec};
+
+ public:
+    static bool set_worker_receiver(JSContext* cx, unsigned argc,
+                                    JS::Value* vp) {
+        JS::CallArgs args = JS::CallArgsFromVp(argc, vp);
+
+        JS::RootedObject global(cx, gjs_get_import_global(cx));
+        JSAutoRealm ar(cx, global);
+
+        JS::RootedObject object(cx);
+        if (!gjs_parse_call_args(cx, "set_receiver", args, "o", "object",
+                                 &object))
+            return false;
+
+        NativeWorker* worker = GetCurrentThreadWorkerPrivate(cx);
+        if (!worker)
+            return false;
+
+        if (!JS_ObjectIsFunction(object))
+            return false;
+
+        gjs_set_global_slot(global, GjsWorkerGlobalSlot::ONMESSAGE,
+                            JS::ObjectValue(*object));
+        return true;
+    };
+
+    static bool get_worker_name(JSContext* cx, unsigned argc, JS::Value* vp) {
+        JS::CallArgs args = JS::CallArgsFromVp(argc, vp);
+
+        JS::RootedObject global(cx, gjs_get_import_global(cx));
+        JSAutoRealm ar(cx, global);
+
+        NativeWorker* worker = GetCurrentThreadWorkerPrivate(cx);
+        if (!worker)
+            return false;
+
+        if (!worker->m_options.m_name.get()) {
+            args.rval().setUndefined();
+            return true;
+        }
+
+        JS::RootedString str(
+            cx, JS_NewStringCopyZ(cx, worker->m_options.m_name.get()));
+        if (!str)
+            return false;
+        args.rval().setString(str);
+        return true;
+    };
+};
+
+namespace WorkerGlobal {
+bool post_message(JSContext* cx, unsigned argc, JS::Value* vp) {
+    JS::CallArgs args = JS::CallArgsFromVp(argc, vp);
+
+    JS::RootedObject global(cx, gjs_get_import_global(cx));
+    JSAutoRealm ar(cx, global);
+
+    JS::RootedValue value(cx, args[0]);
+
+    Gjs::NativeWorker* worker = GetCurrentThreadWorkerPrivate(cx);
+
+    return worker->writeToHost(cx, value);
+}
+
+};  // namespace WorkerGlobal
+
+};  // namespace Gjs
+
+bool gjs_define_worker_stuff(JSContext* cx, JS::MutableHandleObject module) {
+    module.set(JS_NewPlainObject(cx));
+
+    return Gjs::NativeWorker::create_prototype(cx, module) &&
+           JS_DefineFunction(cx, module, "setReceiver",
+                             Gjs::NativeWorker::set_worker_receiver, 1, 0) &&
+           JS_DefineFunction(cx, module, "getName",
+                             Gjs::NativeWorker::get_worker_name, 0, 0);
+}
\ No newline at end of file
diff --git a/gjs/thread.h b/gjs/thread.h
new file mode 100644
index 000000000..3c44dea7a
--- /dev/null
+++ b/gjs/thread.h
@@ -0,0 +1,16 @@
+#pragma once
+
+#include <config.h>
+
+#include <js/RootingAPI.h>
+#include <js/StructuredClone.h>
+#include <js/TypeDecls.h>
+
+namespace Gjs {
+namespace WorkerGlobal {
+bool post_message(JSContext* cx, unsigned argc, JS::Value* vp);
+};
+};  // namespace Gjs
+
+GJS_JSAPI_RETURN_CONVENTION
+bool gjs_define_worker_stuff(JSContext* cx, JS::MutableHandleObject module);
\ No newline at end of file
diff --git a/meson.build b/meson.build
index 136e812b3..ebc57f6c3 100644
--- a/meson.build
+++ b/meson.build
@@ -437,6 +437,7 @@ libgjs_sources = [
     'gjs/profiler.cpp', 'gjs/profiler-private.h',
     'gjs/text-encoding.cpp', 'gjs/text-encoding.h',
     'gjs/promise.cpp', 'gjs/promise.h',
+    'gjs/thread.cpp', 'gjs/thread.h',
     'gjs/stack.cpp',
     'modules/console.cpp', 'modules/console.h',
     'modules/modules.cpp', 'modules/modules.h',
diff --git a/modules/esm/_timers.js b/modules/esm/_timers.js
index 94da6f8f2..1797eaedb 100644
--- a/modules/esm/_timers.js
+++ b/modules/esm/_timers.js
@@ -26,7 +26,10 @@ const timeouts = new Map();
  * @param {GLib.Source} source the source to add to our map
  */
 function addSource(source) {
-    const id = source.attach(null);
+    const context = GLib.MainContext.get_thread_default() ?? null;
+    if (!context)
+        throw new Error('No context available');
+    const id = source.attach(context);
     timeouts.set(source, id);
 }
 
diff --git a/modules/esm/_workers.js b/modules/esm/_workers.js
new file mode 100644
index 000000000..51d128b67
--- /dev/null
+++ b/modules/esm/_workers.js
@@ -0,0 +1,118 @@
+
+const {NativeWorker, setReceiver, getName} = import.meta.importSync('_workerNative');
+
+import {MessageEvent, EventTarget} from './events.js';
+
+function messageReceiver(source, message) {
+    const event = new MessageEvent('message', {
+        source,
+        data: message,
+    });
+
+    source.dispatchEvent(event);
+}
+
+class Worker extends EventTarget {
+    #nativeWorker;
+
+    constructor(uri, options = {}) {
+        super();
+
+        const {type = 'module', name} = options;
+
+        if (type !== 'module')
+            throw Error('Workers must be of type "module"');
+
+        this.#nativeWorker = new NativeWorker(uri, name ?? null);
+        this.#nativeWorker.setReceiver((...args) => messageReceiver(this, ...args));
+    }
+
+    #onerror = null;
+
+    set onerror(handler) {
+        if (this.#onerror)
+            this.removeEventListener('error', this.#onerror);
+        this.addEventListener('error', this.#onerror = handler);
+    }
+
+    #onmessage = null;
+
+    set onmessage(handler) {
+        if (this.#onmessage)
+            this.removeEventListener('message', this.#onmessage);
+        this.addEventListener('message', this.#onmessage = handler);
+    }
+
+    #onmessageerror = null;
+
+    set onmessageerror(handler) {
+        if (this.#onmessageerror)
+            this.removeEventListener('messageerror', this.#onmessageerror);
+        this.addEventListener('messageerror', this.#onmessageerror = handler);
+    }
+
+    // eslint-disable-next-line no-unused-vars
+    postMessage(message, transfer = []) {
+        this.#nativeWorker.write(message);
+    }
+
+    terminate() {
+        this.#nativeWorker.exit();
+    }
+}
+
+// Using `imports` as a flag for the WorkerGlobal for now.
+if ('imports' in globalThis) {
+    Object.defineProperty(globalThis, 'Worker', {
+        configurable: false,
+        enumerable: true,
+        writable: true,
+        value: Worker,
+    });
+} else {
+    let onerror = null;
+    let onmessage = null;
+    let onmessageerror = null;
+    // Make the global an EventTarget
+    let eventTarget = new EventTarget();
+
+    setReceiver((...args) =>
+        messageReceiver(globalThis, ...args));
+
+    const {addEventListener, removeEventListener, dispatchEvent} = eventTarget;
+
+    Object.assign(globalThis, {
+        addEventListener: addEventListener.bind(eventTarget),
+        removeEventListener: removeEventListener.bind(eventTarget),
+        dispatchEvent: dispatchEvent.bind(eventTarget),
+    });
+
+    Object.defineProperties(globalThis, {
+        name: {
+            get() {
+                return getName();
+            },
+        },
+        onerror: {
+            set(handler) {
+                if (onerror)
+                    this.removeEventListener('error', onerror);
+                this.addEventListener('error', onerror = handler);
+            },
+        },
+        onmessage: {
+            set(handler) {
+                if (onmessage)
+                    this.removeEventListener('message', onmessage);
+                this.addEventListener('message', onmessage = handler);
+            },
+        },
+        onmessageerror: {
+            set(handler) {
+                if (onmessageerror)
+                    this.removeEventListener('messageerror', onmessageerror);
+                this.addEventListener('messageerror', onmessageerror = handler);
+            },
+        },
+    });
+}
diff --git a/modules/esm/events.js b/modules/esm/events.js
new file mode 100644
index 000000000..a8b5db9e2
--- /dev/null
+++ b/modules/esm/events.js
@@ -0,0 +1,392 @@
+// SPDX-License-Identifier: MIT OR LGPL-2.0-or-later
+// SPDX-FileCopyrightText: 2022 Evan Welsh <contact evanwelsh com>
+
+class Connection {
+    #instance;
+    #name;
+    #callback;
+    #disconnected;
+
+    /**
+     * @param {object} params _
+     * @param {EventEmitter} params.instance the instance the connection is connected to
+     * @param {string} params.name the name of the signal
+     * @param {Function} params.callback the callback for the signal
+     * @param {boolean} params.disconnected whether the connection is disconnected
+     */
+    constructor({instance, name, callback, disconnected = false}) {
+        this.#instance = instance;
+        this.#name = name;
+        this.#callback = callback;
+        this.#disconnected = disconnected;
+    }
+
+    matches(callback) {
+        return this.#callback === callback;
+    }
+
+    disconnect() {
+        this.#instance.disconnect(this);
+    }
+
+    trigger(...args) {
+        this.#callback.apply(null, [this.#instance, ...args]);
+    }
+
+    get name() {
+        return this.#name;
+    }
+
+    set disconnected(value) {
+        if (!value)
+            throw new Error('Connections cannot be re-connected.');
+
+        this.#disconnected = value;
+    }
+
+    get disconnected() {
+        return this.#disconnected;
+    }
+}
+
+const sEmit = Symbol('private emit');
+
+export class EventEmitter {
+    /** @type {Connection[]} */
+    #signalConnections = [];
+
+    /**
+     * @param {string} name
+     * @param {(...args: any[]) => any} callback
+     * @returns {Connection}
+     */
+    connect(name, callback) {
+        // be paranoid about callback arg since we'd start to throw from emit()
+        // if it was messed up
+        if (typeof callback !== 'function') {
+            throw new Error(
+                'When connecting signal must give a callback that is a function'
+            );
+        }
+
+        const connection = new Connection({
+            instance: this,
+            name,
+            callback,
+        });
+
+        // this makes it O(n) in total connections to emit, but I think
+        // it's right to optimize for low memory and reentrancy-safety
+        // rather than speed
+        this.#signalConnections.push(connection);
+
+        return connection;
+    }
+
+    /**
+     * @param {Connection} connection the connection returned by {@link connect}
+     */
+    disconnect(connection) {
+        if (connection.disconnected) {
+            throw new Error(
+                `Signal handler for ${connection.name} already disconnected`
+            );
+        }
+
+        const index = this.#signalConnections.indexOf(connection);
+        if (index !== -1) {
+            // Mark the connection as disconnected.
+            connection.disconnected = true;
+
+            this.#signalConnections.splice(index, 1);
+            return;
+        }
+
+        throw new Error('No signal connection found for connection');
+    }
+
+    /**
+     * @param {Connection} connection the connection returned by {@link connect}
+     * @returns {boolean} whether the signal connection is connected
+     */
+    signalHandlerIsConnected(connection) {
+        const index = this.#signalConnections.indexOf(connection);
+        return index !== -1 && !connection.disconnected;
+    }
+
+    /**
+     * @param {string} name
+     * @param {(...args: any[]) => any} handler
+     * @returns {Connection | undefined}
+     */
+    findConnectionBySignalHandler(name, handler) {
+        return this.#signalConnections.find(
+            connection =>
+                connection.name === name && connection.matches(handler)
+        );
+    }
+
+    disconnectAll() {
+        while (this.#signalConnections.length > 0)
+            this.#signalConnections[0].disconnect();
+    }
+
+    /**
+     * @param {string} name the signal name to emit
+     * @param {...any} args the arguments to pass
+     */
+    emit(name, ...args) {
+        // To deal with re-entrancy (removal/addition while
+        // emitting), we copy out a list of what was connected
+        // at emission start; and just before invoking each
+        // handler we check its disconnected flag.
+        let handlers = [];
+        let i;
+        let length = this.#signalConnections.length;
+        for (i = 0; i < length; ++i) {
+            let connection = this.#signalConnections[i];
+            if (connection.name === name)
+                handlers.push(connection);
+        }
+
+        length = handlers.length;
+        for (i = 0; i < length; ++i) {
+            let connection = handlers[i];
+
+            if (!connection.disconnected) {
+                try {
+                    // since we pass "null" for this, the global object will be used.
+                    let ret = connection.trigger(...args);
+
+                    // if the callback returns true, we don't call the next
+                    // signal handlers
+                    if (ret === true)
+                        break;
+                } catch (e) {
+                    // just log any exceptions so that callbacks can't disrupt
+                    // signal emission
+                    console.error(
+                        `Exception in callback for signal: ${name}\n`,
+                        e
+                    );
+                }
+            }
+        }
+    }
+}
+
+export class Event {
+    /**
+     * The event is not being processed at this time.
+     */
+    static NONE = 0;
+
+    /**
+     * The event is being propagated through the target's ancestor objects. This process starts with the 
Window, then Document, then the HTMLHtmlElement, and so on through the elements until the target's parent is 
reached. Event listeners registered for capture mode when EventTarget.addEventListener() was called are 
triggered during this phase.
+     */
+    static CAPTURING_PHASE = 1;
+
+    /**
+     * The event has arrived at the event's target. Event listeners registered for this phase are called at 
this time. If Event.bubbles is false, processing the event is finished after this phase is complete.
+     */
+    static AT_TARGET = 2;
+
+    /**
+     * The event is propagating back up through the target's ancestors in reverse order, starting with the 
parent, and eventually reaching the containing Window. This is known as bubbling, and occurs only if 
Event.bubbles is true. Event listeners registered for this phase are triggered during this process.
+     */
+    static BUBBLING_PHASE = 3;
+
+    static {
+        const descriptor = {
+            enumerable: true,
+            configurable: false,
+            writable: false,
+        };
+
+        Object.defineProperties(this, {
+            NONE: {...descriptor, value: this.NONE},
+            CAPTURING_PHASE: {...descriptor, value: this.CAPTURING_PHASE},
+            AT_TARGET: {...descriptor, value: this.AT_TARGET},
+            BUBBLING_PHASE: {...descriptor, value: this.BUBBLING_PHASE},
+        });
+    }
+
+    #type;
+    #bubbles;
+    #cancelable;
+    #composed;
+    #target;
+    #phase = Event.NONE;
+    #timestamp = Date.now();
+
+    constructor(typeArg, eventInit = {}) {
+        const {
+            bubbles = false,
+            cancelable = false,
+            composed = false,
+        } = eventInit;
+
+        this.#type = typeArg;
+        this.#bubbles = bubbles;
+        this.#cancelable = cancelable;
+        this.#composed = composed;
+    }
+
+    cancelBubble = false;
+
+    get bubbles() {
+        return this.#bubbles;
+    }
+
+    get cancelable() {
+        return this.#cancelable;
+    }
+
+    get composed() {
+        return this.#composed;
+    }
+
+    get timeStamp() {
+        return this.#timestamp;
+    }
+
+    get target() {
+        return this.#target;
+    }
+
+    get currentTarget() {
+        return this.target;
+    }
+
+    get defaultPrevented() {
+        return false;
+    }
+
+    get isTrusted() {
+        return false;
+    }
+
+    get type() {
+        return this.#type;
+    }
+
+    get eventPhase() {
+        return this.#phase;
+    }
+
+
+    composedPath() {
+        return this.target ? [this.target] : [];
+    }
+
+    preventDefault() { }
+    stopImmediatePropagation() { }
+    stopPropagation() { }
+
+    [sEmit](target) {
+        this.#target = target;
+        this.#phase = Event.AT_TARGET;
+    }
+}
+
+export class MessageEvent extends Event {
+    #data;
+    #source;
+    #ports;
+    #lastEventId;
+
+    constructor(type, init) {
+        const {data = null, source = null, ports = [], lastEventId = '', ...eventInit} = init;
+        super(type, eventInit);
+
+        this.#data = data;
+        this.#source = source;
+        this.#ports = ports;
+        this.#lastEventId = lastEventId;
+    }
+
+    get data() {
+        return this.#data;
+    }
+
+    get source() {
+        return this.#source;
+    }
+
+    get ports() {
+        return this.#ports;
+    }
+
+    get lastEventId() {
+        return this.#lastEventId;
+    }
+}
+
+export class EventTarget {
+    #emitter = new EventEmitter();
+    #handlers = new Map();
+
+    /**
+     * Registers an event handler of a specific event type on the EventTarget.
+     *
+     * @param type
+     * @param listener
+     */
+    addEventListener(type, listener) {
+        const wrapper = this.#handlers.get(listener) ?? function (instance, event) {
+            listener(event);
+        };
+        this.#handlers.set(listener, wrapper);
+        this.#emitter.connect(type, wrapper);
+    }
+
+    /**
+     * Removes an event listener from the EventTarget.
+     *
+     * @param type
+     * @param listener
+     */
+    removeEventListener(type, listener) {
+        const wrapper = this.#handlers.get(listener);
+
+        if (!wrapper)
+            return;
+
+        const connection = this.#emitter.findConnectionBySignalHandler(
+            type,
+            wrapper
+        );
+
+        connection?.disconnect();
+        this.#handlers.delete(listener);
+    }
+
+    /**
+     * Dispatches an event to this EventTarget.
+     *
+     * @param {Event} event the event to dispatch
+     */
+    dispatchEvent(event) {
+        if (event instanceof Event) {
+            event[sEmit](this);
+            this.#emitter.emit(event.type, event);
+        }
+
+        return true;
+    }
+}
+
+
+Object.defineProperty(globalThis, 'Event', {
+    configurable: false,
+    enumerable: true,
+    writable: true,
+    value: Event,
+});
+
+Object.defineProperty(globalThis, 'EventTarget', {
+    configurable: false,
+    enumerable: true,
+    writable: true,
+    value: EventTarget,
+});


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