[gjs/ewlsh/whatwg-timers] Implement WHATWG Timers API
- From: Evan Welsh <ewlsh src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [gjs/ewlsh/whatwg-timers] Implement WHATWG Timers API
- Date: Fri, 14 Jan 2022 07:41:02 +0000 (UTC)
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]