Skip to content

Commit de0441f

Browse files
committed
lib: implement queueMicrotask
PR-URL: #22951 Refs: https://html.spec.whatwg.org/multipage/timers-and-user-prompts.html#dom-queuemicrotask Reviewed-By: Bradley Farias <bradley.meck@gmail.com> Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
1 parent 59a8324 commit de0441f

File tree

9 files changed

+202
-2
lines changed

9 files changed

+202
-2
lines changed

.eslintrc.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,7 @@ module.exports = {
277277
DTRACE_NET_SERVER_CONNECTION: false,
278278
DTRACE_NET_STREAM_END: false,
279279
TextEncoder: false,
280-
TextDecoder: false
280+
TextDecoder: false,
281+
queueMicrotask: false,
281282
},
282283
};

doc/api/async_hooks.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -240,7 +240,7 @@ FSEVENTWRAP, FSREQWRAP, GETADDRINFOREQWRAP, GETNAMEINFOREQWRAP, HTTPPARSER,
240240
JSSTREAM, PIPECONNECTWRAP, PIPEWRAP, PROCESSWRAP, QUERYWRAP, SHUTDOWNWRAP,
241241
SIGNALWRAP, STATWATCHER, TCPCONNECTWRAP, TCPSERVER, TCPWRAP, TTYWRAP,
242242
UDPSENDWRAP, UDPWRAP, WRITEWRAP, ZLIB, SSLCONNECTION, PBKDF2REQUEST,
243-
RANDOMBYTESREQUEST, TLSWRAP, Timeout, Immediate, TickObject
243+
RANDOMBYTESREQUEST, TLSWRAP, Microtask, Timeout, Immediate, TickObject
244244
```
245245

246246
There is also the `PROMISE` resource type, which is used to track `Promise`

doc/api/globals.md

+40
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,46 @@ added: v0.1.7
107107

108108
The process object. See the [`process` object][] section.
109109

110+
## queueMicrotask(callback)
111+
<!-- YAML
112+
added: REPLACEME
113+
-->
114+
115+
<!-- type=global -->
116+
117+
> Stability: 1 - Experimental
118+
119+
* `callback` {Function} Function to be queued.
120+
121+
The `queueMicrotask()` method queues a microtask to invoke `callback`. If
122+
`callback` throws an exception, the [`process` object][] `'error'` event will
123+
be emitted.
124+
125+
In general, `queueMicrotask` is the idiomatic choice over `process.nextTick()`.
126+
`process.nextTick()` will always run before microtasks, and so unexpected
127+
execution order may be observed.
128+
129+
```js
130+
// Here, `queueMicrotask()` is used to ensure the 'load' event is always
131+
// emitted asynchronously, and therefore consistently. Using
132+
// `process.nextTick()` here would result in the 'load' event always emitting
133+
// before any other promise jobs.
134+
135+
DataHandler.prototype.load = async function load(key) {
136+
const hit = this._cache.get(url);
137+
if (hit !== undefined) {
138+
queueMicrotask(() => {
139+
this.emit('load', hit);
140+
});
141+
return;
142+
}
143+
144+
const data = await fetchData(key);
145+
this._cache.set(url, data);
146+
this.emit('load', data);
147+
};
148+
```
149+
110150
## require()
111151

112152
This variable may appear to be global but is not. See [`require()`].

lib/internal/bootstrap/node.js

+28
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@
134134
setupGlobalConsole();
135135
setupGlobalURL();
136136
setupGlobalEncoding();
137+
setupQueueMicrotask();
137138
}
138139

139140
if (process.binding('config').experimentalWorker) {
@@ -527,6 +528,33 @@
527528
});
528529
}
529530

531+
function setupQueueMicrotask() {
532+
const { queueMicrotask } = NativeModule.require('internal/queue_microtask');
533+
Object.defineProperty(global, 'queueMicrotask', {
534+
get: () => {
535+
process.emitWarning('queueMicrotask() is experimental.',
536+
'ExperimentalWarning');
537+
Object.defineProperty(global, 'queueMicrotask', {
538+
value: queueMicrotask,
539+
writable: true,
540+
enumerable: false,
541+
configurable: true,
542+
});
543+
return queueMicrotask;
544+
},
545+
set: (v) => {
546+
Object.defineProperty(global, 'queueMicrotask', {
547+
value: v,
548+
writable: true,
549+
enumerable: false,
550+
configurable: true,
551+
});
552+
},
553+
enumerable: false,
554+
configurable: true,
555+
});
556+
}
557+
530558
function setupDOMException() {
531559
// Registers the constructor with C++.
532560
NativeModule.require('internal/domexception');

lib/internal/queue_microtask.js

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
'use strict';
2+
3+
const { ERR_INVALID_ARG_TYPE } = require('internal/errors').codes;
4+
const { AsyncResource } = require('async_hooks');
5+
const { getDefaultTriggerAsyncId } = require('internal/async_hooks');
6+
const { internalBinding } = require('internal/bootstrap/loaders');
7+
const { enqueueMicrotask } = internalBinding('util');
8+
9+
// declared separately for name, arrow function to prevent construction
10+
const queueMicrotask = (callback) => {
11+
if (typeof callback !== 'function') {
12+
throw new ERR_INVALID_ARG_TYPE('callback', 'function', callback);
13+
}
14+
15+
const asyncResource = new AsyncResource('Microtask', {
16+
triggerAsyncId: getDefaultTriggerAsyncId(),
17+
requireManualDestroy: true,
18+
});
19+
20+
enqueueMicrotask(() => {
21+
asyncResource.runInAsyncScope(() => {
22+
try {
23+
callback();
24+
} catch (e) {
25+
process.emit('error', e);
26+
}
27+
});
28+
asyncResource.emitDestroy();
29+
});
30+
};
31+
32+
module.exports = { queueMicrotask };

node.gyp

+1
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,7 @@
146146
'lib/internal/querystring.js',
147147
'lib/internal/process/write-coverage.js',
148148
'lib/internal/process/coverage.js',
149+
'lib/internal/queue_microtask.js',
149150
'lib/internal/readline.js',
150151
'lib/internal/repl.js',
151152
'lib/internal/repl/await.js',

src/node_util.cc

+13
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,10 @@ namespace util {
77
using v8::ALL_PROPERTIES;
88
using v8::Array;
99
using v8::Context;
10+
using v8::Function;
1011
using v8::FunctionCallbackInfo;
1112
using v8::Integer;
13+
using v8::Isolate;
1214
using v8::Local;
1315
using v8::Object;
1416
using v8::ONLY_CONFIGURABLE;
@@ -172,6 +174,15 @@ void SafeGetenv(const FunctionCallbackInfo<Value>& args) {
172174
v8::NewStringType::kNormal).ToLocalChecked());
173175
}
174176

177+
void EnqueueMicrotask(const FunctionCallbackInfo<Value>& args) {
178+
Environment* env = Environment::GetCurrent(args);
179+
Isolate* isolate = env->isolate();
180+
181+
CHECK(args[0]->IsFunction());
182+
183+
isolate->EnqueueMicrotask(args[0].As<Function>());
184+
}
185+
175186
void Initialize(Local<Object> target,
176187
Local<Value> unused,
177188
Local<Context> context) {
@@ -219,6 +230,8 @@ void Initialize(Local<Object> target,
219230

220231
env->SetMethod(target, "safeGetenv", SafeGetenv);
221232

233+
env->SetMethod(target, "enqueueMicrotask", EnqueueMicrotask);
234+
222235
Local<Object> constants = Object::New(env->isolate());
223236
NODE_DEFINE_CONSTANT(constants, ALL_PROPERTIES);
224237
NODE_DEFINE_CONSTANT(constants, ONLY_WRITABLE);
+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
'use strict';
2+
const common = require('../common');
3+
4+
const assert = require('assert');
5+
const async_hooks = require('async_hooks');
6+
const initHooks = require('./init-hooks');
7+
const { checkInvocations } = require('./hook-checks');
8+
9+
const hooks = initHooks();
10+
hooks.enable();
11+
12+
const rootAsyncId = async_hooks.executionAsyncId();
13+
14+
queueMicrotask(common.mustCall(function() {
15+
assert.strictEqual(async_hooks.triggerAsyncId(), rootAsyncId);
16+
}));
17+
18+
process.on('exit', function() {
19+
hooks.sanityCheck();
20+
21+
const as = hooks.activitiesOfTypes('Microtask');
22+
checkInvocations(as[0], {
23+
init: 1, before: 1, after: 1, destroy: 1
24+
}, 'when process exits');
25+
});

test/parallel/test-queue-microtask.js

+60
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
'use strict';
2+
3+
const common = require('../common');
4+
const assert = require('assert');
5+
6+
assert.strictEqual(typeof queueMicrotask, 'function');
7+
8+
[
9+
undefined,
10+
null,
11+
0,
12+
'x = 5',
13+
].forEach((t) => {
14+
assert.throws(common.mustCall(() => {
15+
queueMicrotask(t);
16+
}), {
17+
code: 'ERR_INVALID_ARG_TYPE',
18+
});
19+
});
20+
21+
{
22+
let called = false;
23+
queueMicrotask(common.mustCall(() => {
24+
called = true;
25+
}));
26+
assert.strictEqual(called, false);
27+
}
28+
29+
queueMicrotask(common.mustCall(function() {
30+
assert.strictEqual(arguments.length, 0);
31+
}), 'x', 'y');
32+
33+
{
34+
const q = [];
35+
Promise.resolve().then(() => q.push('a'));
36+
queueMicrotask(common.mustCall(() => q.push('b')));
37+
Promise.reject().catch(() => q.push('c'));
38+
39+
queueMicrotask(common.mustCall(() => {
40+
assert.deepStrictEqual(q, ['a', 'b', 'c']);
41+
}));
42+
}
43+
44+
const eq = [];
45+
process.on('error', (e) => {
46+
eq.push(e);
47+
});
48+
49+
process.on('exit', () => {
50+
assert.strictEqual(eq.length, 2);
51+
assert.strictEqual(eq[0].message, 'E1');
52+
assert.strictEqual(
53+
eq[1].message, 'Class constructor cannot be invoked without \'new\'');
54+
});
55+
56+
queueMicrotask(common.mustCall(() => {
57+
throw new Error('E1');
58+
}));
59+
60+
queueMicrotask(class {});

0 commit comments

Comments
 (0)