summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.eslintrc.js3
-rw-r--r--doc/api/async_hooks.md2
-rw-r--r--doc/api/globals.md40
-rw-r--r--lib/internal/bootstrap/node.js28
-rw-r--r--lib/internal/queue_microtask.js32
-rw-r--r--node.gyp1
-rw-r--r--src/node_util.cc13
-rw-r--r--test/async-hooks/test-queue-microtask.js25
-rw-r--r--test/parallel/test-queue-microtask.js60
9 files changed, 202 insertions, 2 deletions
diff --git a/.eslintrc.js b/.eslintrc.js
index 8aa090e889..50b6d5238b 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 4003fb1b02..ff3e14b96c 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 af0bf3ee67..a461dca2ae 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)
+<!-- YAML
+added: REPLACEME
+-->
+
+<!-- type=global -->
+
+> 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 8537131e8b..e1c56e9b7e 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 0000000000..3ff7ae9ae4
--- /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 5f6dfa5364..30fb4ca8ef 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 8f261e8989..c183f314a1 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<Value>& args) {
v8::NewStringType::kNormal).ToLocalChecked());
}
+void EnqueueMicrotask(const FunctionCallbackInfo<Value>& args) {
+ Environment* env = Environment::GetCurrent(args);
+ Isolate* isolate = env->isolate();
+
+ CHECK(args[0]->IsFunction());
+
+ isolate->EnqueueMicrotask(args[0].As<Function>());
+}
+
void Initialize(Local<Object> target,
Local<Value> unused,
Local<Context> context) {
@@ -219,6 +230,8 @@ void Initialize(Local<Object> target,
env->SetMethod(target, "safeGetenv", SafeGetenv);
+ env->SetMethod(target, "enqueueMicrotask", EnqueueMicrotask);
+
Local<Object> 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 0000000000..dfa537752e
--- /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 0000000000..ea9b88c71e
--- /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 {});