[gjs/ewlsh/whatwg-timers] Implement WHATWG Timers API




commit 1274fae136cf621f72f02a7620f162caa61c7e2d
Author: Evan Welsh <contact evanwelsh com>
Date:   Thu Jan 13 23:32:32 2022 -0800

    Implement WHATWG Timers API

 .eslintrc.yml                         |   4 +
 examples/timers.js                    |  21 ++
 gjs/context.cpp                       |   2 +
 gjs/promise.cpp                       |  29 +++
 gjs/promise.h                         |   5 +
 installed-tests/js/.eslintrc.yml      |  20 +-
 installed-tests/js/meson.build        |   1 +
 installed-tests/js/minijasmine.js     |  17 --
 installed-tests/js/testGLib.js        |   2 +-
 installed-tests/js/testLegacyClass.js |   2 +-
 installed-tests/js/testTimers.js      | 403 ++++++++++++++++++++++++++++++++++
 js.gresource.xml                      |   4 +-
 modules/esm/_bootstrap/default.js     |   2 +
 modules/esm/_timers.js                | 204 +++++++++++++++++
 14 files changed, 677 insertions(+), 39 deletions(-)
---
diff --git a/.eslintrc.yml b/.eslintrc.yml
index 26270230..97e728f9 100644
--- a/.eslintrc.yml
+++ b/.eslintrc.yml
@@ -262,5 +262,9 @@ globals:
   TextEncoder: readonly
   TextDecoder: readonly
   console: readonly
+  setTimeout: readonly
+  setInterval: readonly
+  clearTimeout: readonly
+  clearInterval: readonly
 parserOptions:
   ecmaVersion: 2022
diff --git a/examples/timers.js b/examples/timers.js
new file mode 100644
index 00000000..fb23ba00
--- /dev/null
+++ b/examples/timers.js
@@ -0,0 +1,21 @@
+// SPDX-License-Identifier: MIT OR LGPL-2.0-or-later
+// SPDX-FileCopyrightText: 2021 Evan Welsh <contact evanwelsh com>
+
+// This example demonstrates that Promises always execute prior
+// to timeouts. It should log "java" then "script".
+
+const promise = new Promise(r => {
+    let i = 100;
+    while (i--)
+        ;
+
+    r();
+});
+
+setTimeout(() => {
+    promise.then(() => log('java'));
+});
+
+setTimeout(() => {
+    log('script');
+});
diff --git a/gjs/context.cpp b/gjs/context.cpp
index 9fed1f43..07f34679 100644
--- a/gjs/context.cpp
+++ b/gjs/context.cpp
@@ -327,6 +327,8 @@ gjs_context_class_init(GjsContextClass *klass)
         g_irepository_prepend_search_path(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);
diff --git a/gjs/promise.cpp b/gjs/promise.cpp
index ce19a780..0ce4bd7c 100644
--- a/gjs/promise.cpp
+++ b/gjs/promise.cpp
@@ -9,8 +9,15 @@
 #include <gio/gio.h>
 #include <glib-object.h>
 
+#include <js/CallArgs.h>
+#include <js/PropertySpec.h>
+#include <js/RootingAPI.h>
+#include <js/TypeDecls.h>
+#include <jsapi.h>  // for JS_DefineFunctions, JS_NewPlainObject
+
 #include "gjs/context-private.h"
 #include "gjs/jsapi-util.h"
+#include "gjs/macros.h"
 #include "gjs/promise.h"
 
 /**
@@ -175,3 +182,25 @@ void PromiseJobDispatcher::start() {
 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;
+}
+
+JSFunctionSpec gjs_native_promise_module_funcs[] = {
+    JS_FN("drainMicrotaskQueue", &drain_microtask_queue, 0, 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);
+}
diff --git a/gjs/promise.h b/gjs/promise.h
index 8fec2aeb..7752e9d6 100644
--- a/gjs/promise.h
+++ b/gjs/promise.h
@@ -9,6 +9,8 @@
 
 #include <glib.h>
 
+#include <js/TypeDecls.h>
+
 #include "gjs/jsapi-util.h"
 
 class GjsContextPrivate;
@@ -52,3 +54,6 @@ class PromiseJobDispatcher {
 };
 
 };  // namespace Gjs
+
+bool gjs_define_native_promise_stuff(JSContext* cx,
+                                     JS::MutableHandleObject module);
diff --git a/installed-tests/js/.eslintrc.yml b/installed-tests/js/.eslintrc.yml
index bbf09ab2..5f9870b3 100644
--- a/installed-tests/js/.eslintrc.yml
+++ b/installed-tests/js/.eslintrc.yml
@@ -10,25 +10,6 @@ rules:
       message: Do not commit fdescribe(). Use describe() instead.
     - name: fit
       message: Do not commit fit(). Use it() instead.
-  no-restricted-syntax:
-    - error
-    - selector: CallExpression[callee.name="it"] > ArrowFunctionExpression
-      message: Arrow functions can mess up some Jasmine APIs. Use function () instead
-    - selector: CallExpression[callee.name="describe"] > ArrowFunctionExpression
-      message: Arrow functions can mess up some Jasmine APIs. Use function () instead
-    - selector: CallExpression[callee.name="beforeEach"] > ArrowFunctionExpression
-      message: Arrow functions can mess up some Jasmine APIs. Use function () instead
-    - selector: CallExpression[callee.name="afterEach"] > ArrowFunctionExpression
-      message: Arrow functions can mess up some Jasmine APIs. Use function () instead
-    - selector: CallExpression[callee.name="beforeAll"] > ArrowFunctionExpression
-      message: Arrow functions can mess up some Jasmine APIs. Use function () instead
-    - selector: CallExpression[callee.name="afterAll"] > ArrowFunctionExpression
-      message: Arrow functions can mess up some Jasmine APIs. Use function () instead
-globals:
-  clearInterval: writable
-  clearTimeout: writable
-  setInterval: writable
-  setTimeout: writable
 overrides:
   - files:
       - matchers.js
@@ -38,6 +19,7 @@ overrides:
       - testESModules.js
       - testEncoding.js
       - testGLibLogWriter.js
+      - testTimers.js
       - modules/importmeta.js
       - modules/exports.js
       - modules/say.js
diff --git a/installed-tests/js/meson.build b/installed-tests/js/meson.build
index 2f007351..6a5c40b5 100644
--- a/installed-tests/js/meson.build
+++ b/installed-tests/js/meson.build
@@ -236,6 +236,7 @@ modules_tests = [
     'ESModules',
     'Encoding',
     'GLibLogWriter',
+    'Timers',
 ]
 if build_cairo
     modules_tests += 'CairoModule'
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/testGLib.js b/installed-tests/js/testGLib.js
index d8c80a34..4e6ee4a9 100644
--- a/installed-tests/js/testGLib.js
+++ b/installed-tests/js/testGLib.js
@@ -34,7 +34,7 @@ describe('GVariant constructor', function () {
         expect(unpacked[2]).toEqual('asig');
         expect(unpacked[3] instanceof GLib.Variant).toBeTruthy();
         expect(unpacked[3].deepUnpack()).toEqual('variant');
-        expect(unpacked[4] instanceof Array).toBeTruthy();
+        expect(Array.isArray(unpacked[4])).toBeTruthy();
         expect(unpacked[4].length).toEqual(2);
     });
 
diff --git a/installed-tests/js/testLegacyClass.js b/installed-tests/js/testLegacyClass.js
index 4ca1c242..44543027 100644
--- a/installed-tests/js/testLegacyClass.js
+++ b/installed-tests/js/testLegacyClass.js
@@ -326,7 +326,7 @@ describe('Class framework', function () {
 
         let instance = new CustomConstruct(1, 2);
 
-        expect(instance instanceof Array).toBeTruthy();
+        expect(Array.isArray(instance)).toBeTruthy();
         expect(instance instanceof CustomConstruct).toBeFalsy();
         expect(instance).toEqual([1, 2]);
     });
diff --git a/installed-tests/js/testTimers.js b/installed-tests/js/testTimers.js
new file mode 100644
index 00000000..1efeeed8
--- /dev/null
+++ b/installed-tests/js/testTimers.js
@@ -0,0 +1,403 @@
+// SPDX-License-Identifier: MIT OR LGPL-2.0-or-later
+// SPDX-FileCopyrightText: 2018-2019 the Deno authors. All rights reserved.
+// SPDX-FileCopyrightText: 2022 Evan Welsh <contact evanwelsh com>
+
+// Derived from 
https://github.com/denoland/deno/blob/eda6e58520276786bd87e411d0284eb56d9686a6/cli/tests/unit/timers_test.ts
+
+import GLib from 'gi://GLib';
+
+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,
+    };
+}
+
+/**
+ * @param {number} ms the number of milliseconds to wait
+ * @returns {Promise<void>}
+ */
+function waitFor(ms) {
+    return new Promise(resolve => setTimeout(resolve, ms));
+}
+
+/**
+ * @param {(resolve?: () => void, reject?: () => void) => void} callback a callback to call with handlers 
once the promise executes
+ * @returns {jasmine.AsyncMatchers}
+ */
+function expectPromise(callback) {
+    return expectAsync(
+        new Promise((resolve, reject) => {
+            callback(resolve, reject);
+        })
+    );
+}
+
+describe('Timers', () => {
+    it('times out successfully', async () => {
+        const startTime = GLib.get_monotonic_time();
+        const ms = 500;
+        let count = 0;
+        let endTime;
+
+        await expectPromise(resolve => {
+            setTimeout(() => {
+                endTime = GLib.get_monotonic_time();
+                count++;
+
+                resolve();
+            }, ms);
+        }).toBeResolved();
+
+        expect(count).toBe(1);
+        expect(endTime - startTime).toBeGreaterThanOrEqual(ms);
+
+        return 5;
+    });
+
+    it('has correct timeout args', async () => {
+        const arg = 1;
+
+        await expectPromise(resolve => {
+            setTimeout(
+                (a, b, c) => {
+                    expect(a).toBe(arg);
+                    expect(b).toBe(arg.toString());
+                    expect(c).toEqual(jasmine.arrayWithExactContents([arg]));
+
+                    resolve();
+                },
+                10,
+                arg,
+                arg.toString(),
+                [arg]
+            );
+        }).toBeResolved();
+    });
+
+    it('cancels successfully', async () => {
+        let count = 0;
+        const timeout = setTimeout(() => {
+            count++;
+        }, 1);
+        // Cancelled, count should not increment
+        clearTimeout(timeout);
+
+        await waitFor(600);
+
+        expect(count).toBe(0);
+    });
+
+    it('cancels multiple correctly', async () => {
+        const uncalled = jasmine.createSpy('uncalled');
+
+        // 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 waitFor(50);
+
+        expect(uncalled).not.toHaveBeenCalled();
+    });
+
+    it('cancels invalid silent fail', async () => {
+        // 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 () => {
+        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 () => {
+        let count = 0;
+        const id = setInterval(() => {
+            count++;
+        }, 1);
+        clearInterval(id);
+        await waitFor(500);
+        expect(count).toBe(0);
+    });
+
+    it('ordering interval', async () => {
+        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 waitFor(500);
+        expect(timeouts).toBe(1);
+    });
+
+    it('cancel invalid silent fail', () => {
+        // Should silently fail (no panic)
+        clearInterval(2147483647);
+    });
+
+    it('callback this', async () => {
+        const {promise, resolve} = deferred();
+        const obj = {
+            foo() {
+                expect(this).toBe(window);
+                resolve();
+            },
+        };
+        setTimeout(obj.foo, 1);
+        await promise;
+    });
+
+    it('bind this', () => {
+        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('function names match spec', function testFunctionName() {
+        expect(clearTimeout.name).toBe('clearTimeout');
+        expect(clearInterval.name).toBe('clearInterval');
+    });
+
+    it('argument lengths match spec', 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 are unique functions', function clearTimeoutAndClearIntervalNotBeEquals() {
+        expect(clearTimeout).not.toBe(clearInterval);
+    });
+
+    // Based on https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/
+    // and 
https://github.com/web-platform-tests/wpt/blob/7b0ebaccc62b566a1965396e5be7bb2bc06f841f/html/webappapis/scripting/event-loops/task_microtask_ordering.html
+
+    it('microtask ordering', async () => {
+        const executionOrder = [];
+        const expectedExecutionOrder = [
+            'promise',
+            'timeout and promise',
+            'timeout',
+            'callback',
+        ];
+
+        await expectPromise(resolve => {
+            function execute(label) {
+                executionOrder.push(label);
+
+                if (executionOrder.length === expectedExecutionOrder.length)
+                    resolve();
+            }
+
+            setTimeout(() => {
+                execute('timeout');
+            });
+
+            setTimeout(() => {
+                Promise.resolve().then(() => {
+                    execute('timeout and promise');
+                });
+            });
+
+            Promise.resolve().then(() => {
+                execute('promise');
+            });
+
+            execute('callback');
+        }).toBeResolved();
+
+        expect(executionOrder).toEqual(
+            jasmine.arrayWithExactContents(expectedExecutionOrder)
+        );
+    });
+
+    it('nested microtask ordering', async () => {
+        const executionOrder = [];
+        const expectedExecutionOrder = [
+            'promise 1',
+            'promise 2',
+            'promise 3',
+            'promise 4',
+            'promise 4 > nested promise',
+            'promise 4 > returned promise',
+            'timeout 1',
+            'timeout 2',
+            'timeout 3',
+            'timeout 4',
+            'promise 2 > nested timeout',
+            'promise 3 > nested timeout',
+            'promise 3 > nested timeout > nested promise',
+            'timeout 1 > nested timeout',
+            'timeout 2 > nested timeout',
+            'timeout 2 > nested timeout > nested promise',
+            'timeout 3 > nested timeout',
+            'timeout 3 > nested timeout > promise',
+            'timeout 3 > nested timeout > promise > nested timeout',
+        ];
+
+        await expectPromise(resolve => {
+            function execute(label) {
+                executionOrder.push(label);
+            }
+
+            setTimeout(() => {
+                execute('timeout 1');
+                setTimeout(() => {
+                    execute('timeout 1 > nested timeout');
+                });
+            });
+
+            setTimeout(() => {
+                execute('timeout 2');
+                setTimeout(() => {
+                    execute('timeout 2 > nested timeout');
+                    Promise.resolve().then(() => {
+                        execute('timeout 2 > nested timeout > nested promise');
+                    });
+                });
+            });
+
+            setTimeout(() => {
+                execute('timeout 3');
+                setTimeout(() => {
+                    execute('timeout 3 > nested timeout');
+                    Promise.resolve().then(() => {
+                        execute('timeout 3 > nested timeout > promise');
+                        setTimeout(() => {
+                            execute(
+                                'timeout 3 > nested timeout > promise > nested timeout'
+                            );
+                            // The most deeply nested setTimeout will be the last to resolve
+                            // because all queued promises should resolve prior to timeouts
+                            // and timeouts execute in order
+                            resolve();
+                        });
+                    });
+                });
+            });
+
+            setTimeout(() => {
+                execute('timeout 4');
+            });
+
+            Promise.resolve().then(() => {
+                execute('promise 1');
+            });
+
+            Promise.resolve().then(() => {
+                execute('promise 2');
+                setTimeout(() => {
+                    execute('promise 2 > nested timeout');
+                });
+            });
+
+            Promise.resolve().then(() => {
+                execute('promise 3');
+                setTimeout(() => {
+                    execute('promise 3 > nested timeout');
+
+                    Promise.resolve().then(() => {
+                        execute('promise 3 > nested timeout > nested promise');
+                    });
+                });
+            });
+
+            Promise.resolve().then(() => {
+                execute('promise 4');
+
+                Promise.resolve().then(() => {
+                    execute('promise 4 > nested promise');
+                });
+
+                return Promise.resolve().then(() => {
+                    execute('promise 4 > returned promise');
+                });
+            });
+        }).toBeResolved();
+
+        expect(executionOrder).toEqual(
+            jasmine.arrayWithExactContents(expectedExecutionOrder)
+        );
+    });
+});
diff --git a/js.gresource.xml b/js.gresource.xml
index a730f2b8..4d3fde35 100644
--- a/js.gresource.xml
+++ b/js.gresource.xml
@@ -13,7 +13,9 @@
     <file>modules/esm/_encoding/encoding.js</file>
     <file>modules/esm/_encoding/encodingMap.js</file>
     <file>modules/esm/_encoding/util.js</file>
-  
+
+    <file>modules/esm/_timers.js</file>
+
     <file>modules/esm/cairo.js</file>
     <file>modules/esm/gettext.js</file>
     <file>modules/esm/console.js</file>
diff --git a/modules/esm/_bootstrap/default.js b/modules/esm/_bootstrap/default.js
index afb155b0..ff1f28bf 100644
--- a/modules/esm/_bootstrap/default.js
+++ b/modules/esm/_bootstrap/default.js
@@ -7,3 +7,5 @@
 import '_encoding/encoding';
 // Bootstrap the Console API
 import 'console';
+// Bootstrap the Timers API
+import '_timers';
diff --git a/modules/esm/_timers.js b/modules/esm/_timers.js
new file mode 100644
index 00000000..bceee9a1
--- /dev/null
+++ b/modules/esm/_timers.js
@@ -0,0 +1,204 @@
+// SPDX-License-Identifier: MIT OR LGPL-2.0-or-later
+// SPDX-FileCopyrightText: 2021 Evan Welsh <contact evanwelsh com>
+
+/* exported setTimeout, setInterval, clearTimeout, clearInterval */
+/* eslint no-implicit-coercion: ["error", {"allow": ["+"]}] */
+// Note: implicit coercion with + is used to perform the ToNumber algorithm from
+// the timers specification
+
+import GLib from 'gi://GLib';
+import GObject from 'gi://GObject';
+
+const PromiseNative = import.meta.importSync('_promiseNative');
+
+/** @type {Map<Timeout, number>} */
+const timeouts = new Map();
+
+/**
+ * @param {GLib.Source} source the source to wrap in a Timeout
+ * @returns {Timeout}
+ */
+function wrapSource(source) {
+    const timeout = new Timeout(source);
+    const id = source.attach(null);
+
+    timeouts.set(timeout, id);
+
+    return timeout;
+}
+
+const sSource = Symbol('source');
+
+/**
+ * @param {Timeout} timeout the timeout object to remove from our map
+ */
+function releaseTimeout(timeout) {
+    timeouts.delete(timeout);
+    timeout[sSource] = null;
+}
+
+export class Timeout {
+    /** @type {GLib.Source} */
+    [sSource];
+
+    /**
+     * @param {GLib.Source} source a GSource to wrap
+     */
+    constructor(source) {
+        'hide source';
+
+        Object.defineProperty(this, sSource, {
+            configurable: false,
+            enumerable: false,
+            value: source,
+        });
+    }
+}
+
+/**
+ * @param {unknown} thisArg 'this' argument
+ * @returns {asserts thisArg is (null | undefined | typeof globalThis)}
+ */
+function checkThis(thisArg) {
+    if (thisArg !== null && thisArg !== undefined && thisArg !== globalThis)
+        throw new TypeError('Illegal invocation');
+}
+
+/**
+ * @param {number} timeout a timeout in milliseconds
+ * @param {(...args) => any} handler a callback
+ * @returns {GLib.Source}
+ */
+function createTimeoutSource(timeout, handler) {
+    const source = GLib.timeout_source_new(timeout);
+    source.set_priority(GLib.PRIORITY_DEFAULT);
+    GObject.source_set_closure(source, handler);
+
+    return source;
+}
+
+/**
+ * @this {typeof globalThis}
+ * @param {(...args) => any} callback a callback function
+ * @param {number} delay the duration in milliseconds to wait before running callback
+ * @param {...any} args arguments to pass to callback
+ */
+function setTimeout(callback, delay = 0, ...args) {
+    'hide source';
+
+    checkThis(this);
+
+    delay = wrapDelay(+delay);
+    const cb = callback.bind(globalThis, ...args);
+    const timeout = wrapSource(
+        createTimeoutSource(delay, () => {
+            if (!timeouts.has(timeout))
+                return GLib.SOURCE_REMOVE;
+
+            cb();
+            releaseTimeout(timeout);
+            PromiseNative.drainMicrotaskQueue();
+
+            return GLib.SOURCE_REMOVE;
+        })
+    );
+
+    return timeout;
+}
+
+/**
+ * @param {number} delay a number value (in milliseconds)
+ */
+function wrapDelay(delay) {
+    // Zero-fill right shift always returns an unsigned 32-bit integer.
+    return delay >>> 0;
+}
+
+/**
+ * @this {typeof globalThis}
+ * @param {(...args) => any} callback a callback function
+ * @param {number} delay the duration in milliseconds to wait between calling callback
+ * @param {...any} args arguments to pass to callback
+ */
+function setInterval(callback, delay = 0, ...args) {
+    'hide source';
+
+    checkThis(this);
+
+    delay = wrapDelay(+delay);
+    const boundCallback = callback.bind(globalThis, ...args);
+    const timeout = wrapSource(
+        createTimeoutSource(delay, () => {
+            if (!timeouts.has(timeout))
+                return GLib.SOURCE_REMOVE;
+
+            boundCallback();
+            PromiseNative.drainMicrotaskQueue();
+
+            return GLib.SOURCE_CONTINUE;
+        })
+    );
+
+    return timeout;
+}
+
+/**
+ * @param {Timeout} timeout the timeout to clear
+ */
+function _clearTimer(timeout) {
+    if (!timeouts.has(timeout))
+        return;
+
+    const source = timeout[sSource];
+
+    if (source) {
+        source.destroy();
+        releaseTimeout(timeout);
+    }
+}
+
+/**
+ * @param {Timeout} timeout the timeout to clear
+ */
+function clearTimeout(timeout = null) {
+    'hide source';
+
+    _clearTimer(timeout);
+}
+
+/**
+ * @param {Timeout} timeout the timeout to clear
+ */
+function clearInterval(timeout = null) {
+    'hide source';
+
+    _clearTimer(timeout);
+}
+
+Object.defineProperty(globalThis, 'setTimeout', {
+    configurable: false,
+    enumerable: true,
+    writable: true,
+    value: setTimeout,
+});
+
+Object.defineProperty(globalThis, 'setInterval', {
+    configurable: false,
+    enumerable: true,
+    writable: true,
+    value: setInterval,
+});
+
+Object.defineProperty(globalThis, 'clearTimeout', {
+    configurable: false,
+    enumerable: true,
+    writable: true,
+    value: clearTimeout,
+});
+
+Object.defineProperty(globalThis, 'clearInterval', {
+    configurable: false,
+    enumerable: true,
+    writable: true,
+    value: clearInterval,
+});


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