summaryrefslogtreecommitdiff
path: root/gjs/promise.cpp
blob: 39939aa9f42b3b1190bf510c831ca8246e5282af (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
// SPDX-License-Identifier: MIT OR LGPL-2.0-or-later
// SPDX-FileCopyrightText: 2021 Evan Welsh <contact@evanwelsh.com>
// SPDX-FileCopyrightText: 2021 Marco Trevisan <mail@3v1n0.net>

#include <config.h>

#include <stddef.h>  // for size_t

#include <gio/gio.h>
#include <glib-object.h>

#include <js/CallAndConstruct.h>  // for JS::IsCallable
#include <js/CallArgs.h>
#include <js/PropertyAndElement.h>  // for JS_DefineFunctions
#include <js/PropertySpec.h>
#include <js/RootingAPI.h>
#include <js/TypeDecls.h>
#include <jsapi.h>  // for JS_NewPlainObject

#include "gjs/context-private.h"
#include "gjs/jsapi-util-args.h"
#include "gjs/jsapi-util.h"
#include "gjs/macros.h"
#include "gjs/promise.h"

/**
 * promise.cpp - This file implements a custom GSource, PromiseJobQueueSource,
 * which handles promise dispatching within GJS. Custom GSources are able to
 * control under which conditions they dispatch. PromiseJobQueueSource will
 * always dispatch if even a single Promise is enqueued and will continue
 * dispatching until all Promises (also known as "Jobs" within SpiderMonkey)
 * are run. While this does technically mean Promises can starve the mainloop
 * if run recursively, this is intentional. Within JavaScript Promises are
 * considered "microtasks" and a microtask must run before any other task
 * continues.
 *
 * PromiseJobQueueSource is attached to the thread's default GMainContext with
 * a default priority of -1000. This is 10x the priority of G_PRIORITY_HIGH and
 * no application code should attempt to override this.
 *
 * See doc/Custom-GSources.md for more background information on custom
 * GSources and microtasks
 */

namespace Gjs {

/**
 * @brief a custom GSource which handles draining our job queue.
 */
class PromiseJobDispatcher::Source : public GSource {
    // The private GJS context this source runs within.
    GjsContextPrivate* m_gjs;
    // The main context this source attaches to.
    GjsAutoMainContext m_main_context;
    // The cancellable that stops this source.
    GjsAutoUnref<GCancellable> m_cancellable;
    GjsAutoPointer<GSource, GSource, g_source_unref> m_cancellable_source;

    // G_PRIORITY_HIGH is normally -100, we set 10 times that 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.
    static constexpr int PRIORITY = 10 * G_PRIORITY_HIGH;

    // GSource custom functions
    static GSourceFuncs source_funcs;

    // 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.
    gboolean prepare(int* timeout [[maybe_unused]]) { return !m_gjs->empty(); }

    gboolean dispatch() {
        if (g_cancellable_is_cancelled(m_cancellable))
            return G_SOURCE_REMOVE;

        // 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(this, -1);

        // A reference to the current cancellable is needed in case any
        // jobs reset PromiseJobDispatcher and thus replace the cancellable.
        GjsAutoUnref<GCancellable> cancellable(m_cancellable,
                                               GjsAutoTakeOwnership{});
        // Drain the job queue.
        m_gjs->runJobs(m_gjs->context(), cancellable);

        return G_SOURCE_CONTINUE;
    }

 public:
    /**
     * @brief Constructs a new GjsPromiseJobQueueSource GSource and adds a
     * reference to the associated main context.
     *
     * @param cx the current JSContext
     * @param cancellable an optional cancellable
     */
    Source(GjsContextPrivate* gjs, GMainContext* main_context)
        : m_gjs(gjs),
          m_main_context(main_context, GjsAutoTakeOwnership()),
          m_cancellable(g_cancellable_new()),
          m_cancellable_source(g_cancellable_source_new(m_cancellable)) {
        g_source_set_priority(this, PRIORITY);
#if GLIB_CHECK_VERSION(2, 70, 0)
        g_source_set_static_name(this, "GjsPromiseJobQueueSource");
#else
        g_source_set_name(this, "GjsPromiseJobQueueSource");
#endif

        // Add our cancellable source to our main source,
        // this will trigger the main source if our cancellable
        // is cancelled.
        g_source_add_child_source(this, m_cancellable_source);
    }

    void* operator new(size_t size) {
        return g_source_new(&source_funcs, size);
    }
    void operator delete(void* p) { g_source_unref(static_cast<GSource*>(p)); }

    bool is_running() { return !!g_source_get_context(this); }

    /**
     * @brief Trigger the cancellable, detaching our source.
     */
    void cancel() { g_cancellable_cancel(m_cancellable); }
    /**
     * @brief Reset the cancellable and prevent the source from stopping,
     * overriding a previous cancel() call. Called by start() in
     * PromiseJobDispatcher to ensure the custom source will start.
     */
    void reset() {
        if (!g_cancellable_is_cancelled(m_cancellable))
            return;

        if (is_running())
            g_source_remove_child_source(this, m_cancellable_source);
        else
            g_source_destroy(m_cancellable_source);

        // Drop the old cancellable and create a new one, as per
        // https://docs.gtk.org/gio/method.Cancellable.reset.html
        m_cancellable = g_cancellable_new();
        m_cancellable_source = g_cancellable_source_new(m_cancellable);
        g_source_add_child_source(this, m_cancellable_source);
    }
};

GSourceFuncs PromiseJobDispatcher::Source::source_funcs = {
    [](GSource* source, int* timeout) {
        return static_cast<Source*>(source)->prepare(timeout);
    },
    nullptr,  // check
    [](GSource* source, GSourceFunc, void*) {
        return static_cast<Source*>(source)->dispatch();
    },
    [](GSource* source) { static_cast<Source*>(source)->~Source(); },
};

PromiseJobDispatcher::PromiseJobDispatcher(GjsContextPrivate* gjs)
    // Acquire a guaranteed reference to this thread's default main context
    : m_main_context(g_main_context_ref_thread_default()),
      // Create and reference our custom GSource
      m_source(std::make_unique<Source>(gjs, m_main_context)) {}

PromiseJobDispatcher::~PromiseJobDispatcher() {
    g_source_destroy(m_source.get());
}

bool PromiseJobDispatcher::is_running() { return m_source->is_running(); }

void PromiseJobDispatcher::start() {
    // Reset the cancellable
    m_source->reset();

    // Don't re-attach if the task is already running
    if (is_running())
        return;

    g_source_attach(m_source.get(), m_main_context);
}

void PromiseJobDispatcher::stop() { m_source->cancel(); }

};  // namespace Gjs

GJS_JSAPI_RETURN_CONVENTION
bool drain_microtask_queue(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;
}

GJS_JSAPI_RETURN_CONVENTION
bool set_main_loop_hook(JSContext* cx, unsigned argc, JS::Value* vp) {
    JS::CallArgs args = JS::CallArgsFromVp(argc, vp);

    JS::RootedObject callback(cx);
    if (!gjs_parse_call_args(cx, "setMainLoopHook", args, "o", "callback",
                             &callback)) {
        return false;
    }

    if (!JS::IsCallable(callback)) {
        gjs_throw(cx, "Main loop hook must be callable");
        return false;
    }

    GjsContextPrivate* priv = GjsContextPrivate::from_cx(cx);
    if (!priv->set_main_loop_hook(callback)) {
        gjs_throw(
            cx,
            "A mainloop is already running. Did you already call runAsync()?");
        return false;
    }

    args.rval().setUndefined();
    return true;
}

JSFunctionSpec gjs_native_promise_module_funcs[] = {
    JS_FN("drainMicrotaskQueue", &drain_microtask_queue, 0, 0),
    JS_FN("setMainLoopHook", &set_main_loop_hook, 1, 0), JS_FS_END};

bool gjs_define_native_promise_stuff(JSContext* cx,
                                     JS::MutableHandleObject module) {
    module.set(JS_NewPlainObject(cx));
    if (!module)
        return false;
    return JS_DefineFunctions(cx, module, gjs_native_promise_module_funcs);
}