Skip to content

Commit

Permalink
lib: implement queueMicrotask
Browse files Browse the repository at this point in the history
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>
  • Loading branch information
devsnek committed Sep 23, 2018
1 parent 59a8324 commit de0441f
Show file tree
Hide file tree
Showing 9 changed files with 202 additions and 2 deletions.
3 changes: 2 additions & 1 deletion .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,7 @@ module.exports = {
DTRACE_NET_SERVER_CONNECTION: false,
DTRACE_NET_STREAM_END: false,
TextEncoder: false,
TextDecoder: false
TextDecoder: false,
queueMicrotask: false,
},
};
2 changes: 1 addition & 1 deletion doc/api/async_hooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down
40 changes: 40 additions & 0 deletions doc/api/globals.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

This comment has been minimized.

Copy link
@addaleax

addaleax Sep 23, 2018

Member

@devsnek

Promise.resolve().then(() => {
  Promise.resolve().then(() => console.log('promise'));
  process.nextTick(() => console.log('nexttick'))
});

gives

promise
nexttick

whereas

setTimeout(() => {
  Promise.resolve().then(() => console.log('promise'));
  process.nextTick(() => console.log('nexttick'))
});

gives

nexttick
promise

So this doesn’t seem to be accurate?


```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()`].
Expand Down
28 changes: 28 additions & 0 deletions lib/internal/bootstrap/node.js
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@
setupGlobalConsole();
setupGlobalURL();
setupGlobalEncoding();
setupQueueMicrotask();
}

if (process.binding('config').experimentalWorker) {
Expand Down Expand Up @@ -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');
Expand Down
32 changes: 32 additions & 0 deletions lib/internal/queue_microtask.js
Original file line number Diff line number Diff line change
@@ -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 };
1 change: 1 addition & 0 deletions node.gyp
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
13 changes: 13 additions & 0 deletions src/node_util.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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);
Expand Down
25 changes: 25 additions & 0 deletions test/async-hooks/test-queue-microtask.js
Original file line number Diff line number Diff line change
@@ -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');
});
60 changes: 60 additions & 0 deletions test/parallel/test-queue-microtask.js
Original file line number Diff line number Diff line change
@@ -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 {});

0 comments on commit de0441f

Please sign in to comment.