[gjs/ewlsh/whatwg-timers: 48/48] temp: Implement WHATWG Timers API
- From: Evan Welsh <ewlsh src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [gjs/ewlsh/whatwg-timers: 48/48] temp: Implement WHATWG Timers API
- Date: Fri, 14 Jan 2022 07:06:08 +0000 (UTC)
commit 17615fcc89168a5a5451ee9fb28457f34c28cdaf
Author: Evan Welsh <contact evanwelsh com>
Date: Sat Oct 30 15:38:39 2021 -0700
temp: Implement WHATWG Timers API
.eslintrc.yml | 4 +
examples/timers.js | 18 ++
gjs/context.cpp | 2 +
gjs/promise.cpp | 29 +++
gjs/promise.h | 5 +
installed-tests/js/.eslintrc.yml | 19 --
installed-tests/js/meson.build | 1 +
installed-tests/js/minijasmine.js | 17 --
installed-tests/js/testTimers.js | 378 ++++++++++++++++++++++++++++++++++++++
js.gresource.xml | 4 +-
modules/esm/_bootstrap/default.js | 2 +
modules/esm/_timers.js | 204 ++++++++++++++++++++
node.js | 136 ++++++++++++++
13 files changed, 782 insertions(+), 37 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..12816c4d
--- /dev/null
+++ b/examples/timers.js
@@ -0,0 +1,18 @@
+// SPDX-License-Identifier: MIT OR LGPL-2.0-or-later
+// SPDX-FileCopyrightText: 2021 Evan Welsh <contact evanwelsh com>
+
+const promise = new Promise(r => {
+ let i = 100;
+ while (i--)
+ ;
+
+ r();
+});
+
+setTimeout(() => {
+ promise.then(() => log('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..f2226d9a 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
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/testTimers.js b/installed-tests/js/testTimers.js
new file mode 100644
index 00000000..c952733c
--- /dev/null
+++ b/installed-tests/js/testTimers.js
@@ -0,0 +1,378 @@
+// SPDX-License-Identifier: MIT OR LGPL-2.0-or-later
+// SPDX-FileCopyrightText: 2018-2019 the Deno authors. All rights reserved.
+
+import GLib from 'gi://GLib';
+
+/**
+ * @param {number} ms
+ * @returns {Promise<void>}
+ */
+function waitFor(ms) {
+ return new Promise((resolve) => setTimeout(resolve, ms));
+}
+
+/**
+ *
+ * @param {(resolve?: () => void, reject?: () => void) => void} callback
+ * @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', // eslint-disable-next-line require-await
+ async () => {
+ // 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', // eslint-disable-next-line require-await
+ async () => {
+ 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..5f2b5ada
--- /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
+ */
+function releaseTimeout(timeout) {
+ timeouts.delete(timeout);
+ timeout[sSource] = null;
+}
+
+export class Timeout {
+ /** @type {GLib.Source} */
+ [sSource];
+
+ /**
+ * @param {GLib.Source} source
+ */
+ 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
+ * @param {(...args) => any} handler
+ * @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,
+});
diff --git a/node.js b/node.js
new file mode 100644
index 00000000..97f15716
--- /dev/null
+++ b/node.js
@@ -0,0 +1,136 @@
+//async function go() {
+if (typeof console === 'undefined') {
+ const GLib = imports.gi.GLib;
+ 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;
+
+ globalThis.console = { log: print };
+}
+
+const executionOrder = [];
+async function go() {
+ 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 (new Promise((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');
+ });
+ });
+ }));
+}
+
+
+
+go().then(() => { console.log('do it') }).then(() => { console.log(JSON.stringify(executionOrder, null, 4));
}).finally(() => {
+ if (typeof imports !== 'undefined' && imports.mainloop) imports.mainloop.quit();
+});
+
+if (typeof imports !== 'undefined' && imports.mainloop)
+ imports.mainloop.run();
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]