From de0441f6f677b338b78752b8fbaac767fdf35075 Mon Sep 17 00:00:00 2001 From: Gus Caplan Date: Wed, 19 Sep 2018 10:33:52 -0500 Subject: [PATCH] lib: implement queueMicrotask PR-URL: https://github.com/nodejs/node/pull/22951 Refs: https://html.spec.whatwg.org/multipage/timers-and-user-prompts.html#dom-queuemicrotask Reviewed-By: Bradley Farias Reviewed-By: James M Snell Reviewed-By: Matteo Collina --- .eslintrc.js | 3 +- doc/api/async_hooks.md | 2 +- doc/api/globals.md | 40 ++++++++++++++++ lib/internal/bootstrap/node.js | 28 +++++++++++ lib/internal/queue_microtask.js | 32 +++++++++++++ node.gyp | 1 + src/node_util.cc | 13 +++++ test/async-hooks/test-queue-microtask.js | 25 ++++++++++ test/parallel/test-queue-microtask.js | 60 ++++++++++++++++++++++++ 9 files changed, 202 insertions(+), 2 deletions(-) create mode 100644 lib/internal/queue_microtask.js create mode 100644 test/async-hooks/test-queue-microtask.js create mode 100644 test/parallel/test-queue-microtask.js diff --git a/.eslintrc.js b/.eslintrc.js index 8aa090e889d70b..50b6d5238b9a05 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -277,6 +277,7 @@ module.exports = { DTRACE_NET_SERVER_CONNECTION: false, DTRACE_NET_STREAM_END: false, TextEncoder: false, - TextDecoder: false + TextDecoder: false, + queueMicrotask: false, }, }; diff --git a/doc/api/async_hooks.md b/doc/api/async_hooks.md index 4003fb1b0226a5..ff3e14b96c250e 100644 --- a/doc/api/async_hooks.md +++ b/doc/api/async_hooks.md @@ -240,7 +240,7 @@ FSEVENTWRAP, FSREQWRAP, GETADDRINFOREQWRAP, GETNAMEINFOREQWRAP, HTTPPARSER, JSSTREAM, PIPECONNECTWRAP, PIPEWRAP, PROCESSWRAP, QUERYWRAP, SHUTDOWNWRAP, SIGNALWRAP, STATWATCHER, TCPCONNECTWRAP, TCPSERVER, TCPWRAP, TTYWRAP, UDPSENDWRAP, UDPWRAP, WRITEWRAP, ZLIB, SSLCONNECTION, PBKDF2REQUEST, -RANDOMBYTESREQUEST, TLSWRAP, Timeout, Immediate, TickObject +RANDOMBYTESREQUEST, TLSWRAP, Microtask, Timeout, Immediate, TickObject ``` There is also the `PROMISE` resource type, which is used to track `Promise` diff --git a/doc/api/globals.md b/doc/api/globals.md index af0bf3ee674c16..a461dca2ae2f3d 100644 --- a/doc/api/globals.md +++ b/doc/api/globals.md @@ -107,6 +107,46 @@ added: v0.1.7 The process object. See the [`process` object][] section. +## queueMicrotask(callback) + + + + +> Stability: 1 - Experimental + +* `callback` {Function} Function to be queued. + +The `queueMicrotask()` method queues a microtask to invoke `callback`. If +`callback` throws an exception, the [`process` object][] `'error'` event will +be emitted. + +In general, `queueMicrotask` is the idiomatic choice over `process.nextTick()`. +`process.nextTick()` will always run before microtasks, and so unexpected +execution order may be observed. + +```js +// Here, `queueMicrotask()` is used to ensure the 'load' event is always +// emitted asynchronously, and therefore consistently. Using +// `process.nextTick()` here would result in the 'load' event always emitting +// before any other promise jobs. + +DataHandler.prototype.load = async function load(key) { + const hit = this._cache.get(url); + if (hit !== undefined) { + queueMicrotask(() => { + this.emit('load', hit); + }); + return; + } + + const data = await fetchData(key); + this._cache.set(url, data); + this.emit('load', data); +}; +``` + ## require() This variable may appear to be global but is not. See [`require()`]. diff --git a/lib/internal/bootstrap/node.js b/lib/internal/bootstrap/node.js index 8537131e8be9a7..e1c56e9b7e6679 100644 --- a/lib/internal/bootstrap/node.js +++ b/lib/internal/bootstrap/node.js @@ -134,6 +134,7 @@ setupGlobalConsole(); setupGlobalURL(); setupGlobalEncoding(); + setupQueueMicrotask(); } if (process.binding('config').experimentalWorker) { @@ -527,6 +528,33 @@ }); } + function setupQueueMicrotask() { + const { queueMicrotask } = NativeModule.require('internal/queue_microtask'); + Object.defineProperty(global, 'queueMicrotask', { + get: () => { + process.emitWarning('queueMicrotask() is experimental.', + 'ExperimentalWarning'); + Object.defineProperty(global, 'queueMicrotask', { + value: queueMicrotask, + writable: true, + enumerable: false, + configurable: true, + }); + return queueMicrotask; + }, + set: (v) => { + Object.defineProperty(global, 'queueMicrotask', { + value: v, + writable: true, + enumerable: false, + configurable: true, + }); + }, + enumerable: false, + configurable: true, + }); + } + function setupDOMException() { // Registers the constructor with C++. NativeModule.require('internal/domexception'); diff --git a/lib/internal/queue_microtask.js b/lib/internal/queue_microtask.js new file mode 100644 index 00000000000000..3ff7ae9ae48702 --- /dev/null +++ b/lib/internal/queue_microtask.js @@ -0,0 +1,32 @@ +'use strict'; + +const { ERR_INVALID_ARG_TYPE } = require('internal/errors').codes; +const { AsyncResource } = require('async_hooks'); +const { getDefaultTriggerAsyncId } = require('internal/async_hooks'); +const { internalBinding } = require('internal/bootstrap/loaders'); +const { enqueueMicrotask } = internalBinding('util'); + +// declared separately for name, arrow function to prevent construction +const queueMicrotask = (callback) => { + if (typeof callback !== 'function') { + throw new ERR_INVALID_ARG_TYPE('callback', 'function', callback); + } + + const asyncResource = new AsyncResource('Microtask', { + triggerAsyncId: getDefaultTriggerAsyncId(), + requireManualDestroy: true, + }); + + enqueueMicrotask(() => { + asyncResource.runInAsyncScope(() => { + try { + callback(); + } catch (e) { + process.emit('error', e); + } + }); + asyncResource.emitDestroy(); + }); +}; + +module.exports = { queueMicrotask }; diff --git a/node.gyp b/node.gyp index 5f6dfa5364d294..30fb4ca8ef08c6 100644 --- a/node.gyp +++ b/node.gyp @@ -146,6 +146,7 @@ 'lib/internal/querystring.js', 'lib/internal/process/write-coverage.js', 'lib/internal/process/coverage.js', + 'lib/internal/queue_microtask.js', 'lib/internal/readline.js', 'lib/internal/repl.js', 'lib/internal/repl/await.js', diff --git a/src/node_util.cc b/src/node_util.cc index 8f261e8989de39..c183f314a1394f 100644 --- a/src/node_util.cc +++ b/src/node_util.cc @@ -7,8 +7,10 @@ namespace util { using v8::ALL_PROPERTIES; using v8::Array; using v8::Context; +using v8::Function; using v8::FunctionCallbackInfo; using v8::Integer; +using v8::Isolate; using v8::Local; using v8::Object; using v8::ONLY_CONFIGURABLE; @@ -172,6 +174,15 @@ void SafeGetenv(const FunctionCallbackInfo& args) { v8::NewStringType::kNormal).ToLocalChecked()); } +void EnqueueMicrotask(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + Isolate* isolate = env->isolate(); + + CHECK(args[0]->IsFunction()); + + isolate->EnqueueMicrotask(args[0].As()); +} + void Initialize(Local target, Local unused, Local context) { @@ -219,6 +230,8 @@ void Initialize(Local target, env->SetMethod(target, "safeGetenv", SafeGetenv); + env->SetMethod(target, "enqueueMicrotask", EnqueueMicrotask); + Local constants = Object::New(env->isolate()); NODE_DEFINE_CONSTANT(constants, ALL_PROPERTIES); NODE_DEFINE_CONSTANT(constants, ONLY_WRITABLE); diff --git a/test/async-hooks/test-queue-microtask.js b/test/async-hooks/test-queue-microtask.js new file mode 100644 index 00000000000000..dfa537752e37fc --- /dev/null +++ b/test/async-hooks/test-queue-microtask.js @@ -0,0 +1,25 @@ +'use strict'; +const common = require('../common'); + +const assert = require('assert'); +const async_hooks = require('async_hooks'); +const initHooks = require('./init-hooks'); +const { checkInvocations } = require('./hook-checks'); + +const hooks = initHooks(); +hooks.enable(); + +const rootAsyncId = async_hooks.executionAsyncId(); + +queueMicrotask(common.mustCall(function() { + assert.strictEqual(async_hooks.triggerAsyncId(), rootAsyncId); +})); + +process.on('exit', function() { + hooks.sanityCheck(); + + const as = hooks.activitiesOfTypes('Microtask'); + checkInvocations(as[0], { + init: 1, before: 1, after: 1, destroy: 1 + }, 'when process exits'); +}); diff --git a/test/parallel/test-queue-microtask.js b/test/parallel/test-queue-microtask.js new file mode 100644 index 00000000000000..ea9b88c71e2966 --- /dev/null +++ b/test/parallel/test-queue-microtask.js @@ -0,0 +1,60 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); + +assert.strictEqual(typeof queueMicrotask, 'function'); + +[ + undefined, + null, + 0, + 'x = 5', +].forEach((t) => { + assert.throws(common.mustCall(() => { + queueMicrotask(t); + }), { + code: 'ERR_INVALID_ARG_TYPE', + }); +}); + +{ + let called = false; + queueMicrotask(common.mustCall(() => { + called = true; + })); + assert.strictEqual(called, false); +} + +queueMicrotask(common.mustCall(function() { + assert.strictEqual(arguments.length, 0); +}), 'x', 'y'); + +{ + const q = []; + Promise.resolve().then(() => q.push('a')); + queueMicrotask(common.mustCall(() => q.push('b'))); + Promise.reject().catch(() => q.push('c')); + + queueMicrotask(common.mustCall(() => { + assert.deepStrictEqual(q, ['a', 'b', 'c']); + })); +} + +const eq = []; +process.on('error', (e) => { + eq.push(e); +}); + +process.on('exit', () => { + assert.strictEqual(eq.length, 2); + assert.strictEqual(eq[0].message, 'E1'); + assert.strictEqual( + eq[1].message, 'Class constructor cannot be invoked without \'new\''); +}); + +queueMicrotask(common.mustCall(() => { + throw new Error('E1'); +})); + +queueMicrotask(class {});