Skip to content

Commit

Permalink
util: expose promise events
Browse files Browse the repository at this point in the history
  • Loading branch information
devsnek committed Aug 9, 2018
1 parent c7962dc commit 3dec7bb
Show file tree
Hide file tree
Showing 10 changed files with 435 additions and 37 deletions.
74 changes: 74 additions & 0 deletions doc/api/util.md
Original file line number Diff line number Diff line change
Expand Up @@ -1631,6 +1631,80 @@ const module = new WebAssembly.Module(wasmBuffer);
util.types.isWebAssemblyCompiledModule(module); // Returns true
```

### util.createPromiseHook(hooks)
<!-- YAML
added: REPLACEME
-->

> Stability: 1 - Experimental
* `hooks` {Object}
* `resolve` {Function} Called when a promise resolve function is called. The
promise is not fulfilled or rejected at this point.
* `rejectWithNoHandler` {Function} Called when a promise rejects without any
handler.
* `handlerAddedAfterReject` {Function} Called when a promise handler is added
to a promise that rejected without a handler.
* `rejectAfterResolved` {Function} Called when a promise reject function is
called after resolution.
* `resolveAfterResolved` {Function} Called when a promise resolve function is
called after resolution.

Returns: {Object}
* `enable` {Function} Enables the hook
* `disable` {Function} Disables the hook

Registers the promise hook `hook` to be called for certain promise debugging
events.

```js
util.createPromiseHook({
resolve(promise) {
console.log('Promise did resolve');
},
rejectWithNoHandler(promise, value) {
console.log('Promise did rejectWithNoHandler with', value);
},
handlerAddedAfterReject(promise) {
console.log('Promise did handlerAddedAfterReject');
},
}).enable();

const x = Promise.resolve('success')
.then(() => Promise.reject('error'));

setTimeout(() => {
x.catch(() => {});
}, 10);

// Promise did resolve
// Promise did resolve
// Promise did rejectWithNoHandler with 'error'
// Promise did resolve
// Promise did handlerAddedAfterReject
// Promise did resolve
// Promise did rejectWithNoHandler with 'error'
// Promise did resolve
// Promise did handlerAddedAfterReject
// Promise did resolve
```

```js
util.createPromiseHook({
rejectAfterResolved(promise, value) {
console.log('Promise did rejectAfterResolved with', value);
},
}).enable();

new Promise((resolve, reject) => {
resolve('success');

reject('oops');
});

// Promise did rejectAfterResolved with 'oops'
```

## Deprecated APIs

The following APIs are deprecated and should no longer be used. Existing
Expand Down
71 changes: 70 additions & 1 deletion lib/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@

'use strict';

const { internalBinding } = require('internal/bootstrap/loaders');
const errors = require('internal/errors');
const {
ERR_FALSY_VALUE_REJECTION,
Expand All @@ -37,8 +38,8 @@ const {
kRejected,
previewEntries
} = process.binding('util');
const { setPromiseHook } = internalBinding('safe_util');

const { internalBinding } = require('internal/bootstrap/loaders');
const types = internalBinding('types');
Object.assign(types, require('internal/util/types'));
const {
Expand Down Expand Up @@ -1464,6 +1465,72 @@ function getSystemErrorName(err) {
return internalErrorName(err);
}

const promiseEventTypes = [
'init',
'resolve',
'before',
'after',
'rejectWithNoHandler',
'handlerAddedAfterReject',
'rejectAfterResolved',
'resolveAfterResolved',
];
const promiseHooks = new Set();
let promiseHookWarned = false;
let resolveHookRef = 0;
function promiseHookCallback(type, promise, value) {
type = promiseEventTypes[type];
for (const hooks of promiseHooks) {
try {
const hook = hooks[type];
if (typeof hook === 'function') {
hook(promise, value);
}
} catch (e) {
process.nextTick(() => { throw e; });
}
}
}
function createPromiseHook(hooks) {
if (!promiseHookWarned) {
promiseHookWarned = true;
process.emitWarning('The util.createPromiseHook API is experimental.',
'ExperimentalWarning');
}

const hasResolve = 'resolve' in hooks;

promiseEventTypes.forEach((type) => {
const hook = hooks[type];
if (hook === undefined) {
return;
}

if (typeof hook !== 'function') {
throw new ERR_INVALID_ARG_TYPE(`hooks.${type}`, 'function', hook);
}
});

return {
enable() {
promiseHooks.add(hooks);
if (hasResolve) {
resolveHookRef++;
}
setPromiseHook(promiseHookCallback, resolveHookRef > 0);
},
disable() {
promiseHooks.delete(hooks);
if (hasResolve) {
resolveHookRef--;
}
setPromiseHook(
hooks.size === 0 ? null : promiseHookCallback,
resolveHookRef > 0);
},
};
}

// Keep the `exports =` so that various functions can still be monkeypatched
module.exports = exports = {
_errnoException: errors.errnoException,
Expand Down Expand Up @@ -1505,6 +1572,8 @@ module.exports = exports = {
TextEncoder,
types,

createPromiseHook,

// Deprecated Old Stuff
debug: deprecate(debug,
'util.debug is deprecated. Use console.error instead.',
Expand Down
34 changes: 13 additions & 21 deletions src/bootstrapper.cc
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,6 @@ using v8::Local;
using v8::MaybeLocal;
using v8::Object;
using v8::Promise;
using v8::PromiseRejectEvent;
using v8::PromiseRejectMessage;
using v8::String;
using v8::Value;

Expand Down Expand Up @@ -52,30 +50,21 @@ void SetupNextTick(const FunctionCallbackInfo<Value>& args) {
args.GetReturnValue().Set(ret);
}

void PromiseRejectCallback(PromiseRejectMessage message) {
void PromiseRejectCallback(PromiseHookType type,
Local<Promise> promise,
Local<Value> value,
void* arg) {
static std::atomic<uint64_t> unhandledRejections{0};
static std::atomic<uint64_t> rejectionsHandledAfter{0};

Local<Promise> promise = message.GetPromise();
Isolate* isolate = promise->GetIsolate();
PromiseRejectEvent event = message.GetEvent();

Environment* env = Environment::GetCurrent(isolate);
Environment* env = static_cast<Environment*>(arg);
Local<Function> callback;
Local<Value> value;

if (event == v8::kPromiseRejectWithNoHandler) {
if (type == PromiseHookType::kRejectWithNoHandler) {
callback = env->promise_reject_unhandled_function();
value = message.GetValue();

if (value.IsEmpty())
value = Undefined(isolate);

unhandledRejections++;
} else if (event == v8::kPromiseHandlerAddedAfterReject) {
} else if (type == PromiseHookType::kHandlerAddedAfterReject) {
callback = env->promise_reject_handled_function();
value = Undefined(isolate);

rejectionsHandledAfter++;
} else {
return;
Expand All @@ -86,10 +75,13 @@ void PromiseRejectCallback(PromiseRejectMessage message) {
"unhandled", unhandledRejections,
"handledAfter", rejectionsHandledAfter);

if (value.IsEmpty()) {
value = Undefined(env->isolate());
}

Local<Value> args[] = { promise, value };
MaybeLocal<Value> ret = callback->Call(env->context(),
Undefined(isolate),
Undefined(env->isolate()),
arraysize(args),
args);

Expand All @@ -99,14 +91,14 @@ void PromiseRejectCallback(PromiseRejectMessage message) {

void SetupPromises(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
Isolate* isolate = env->isolate();

CHECK(args[0]->IsFunction());
CHECK(args[1]->IsFunction());

isolate->SetPromiseRejectCallback(PromiseRejectCallback);
env->set_promise_reject_unhandled_function(args[0].As<Function>());
env->set_promise_reject_handled_function(args[1].As<Function>());

env->AddPromiseRejectHook(PromiseRejectCallback, env);
}

#define BOOTSTRAP_METHOD(name, fn) env->SetMethod(bootstrapper, #name, fn)
Expand Down
Loading

0 comments on commit 3dec7bb

Please sign in to comment.