New Case Study:See how Anthropic automated 95% of dependency reviews with Socket.Learn More
Socket
Sign inDemoInstall
Socket

async-hook-domain

Package Overview
Dependencies
Maintainers
1
Versions
15
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

async-hook-domain - npm Package Compare versions

Comparing version 2.0.4 to 3.0.0

264

index.js

@@ -6,23 +6,49 @@ const { executionAsyncId, createHook } = require('async_hooks')

// things are quite common.
const proc = typeof process === 'object' && process ? process : /* istanbul ignore next */ {
env: {},
emit: /* istanbul ignore next */ () => {},
once: /* istanbul ignore next */ () => {},
_fatalException: /* istanbul ignore next */ () => {},
}
const proc =
typeof process === 'object' && process
? process
: /* istanbul ignore next */ {
_handler: null,
env: {},
execArgv: [],
hasUncaughtExceptionCaptureCallback: /* istanbul ignore next */ () =>
!!proc._handler,
setUncaughtExceptionCaptureCallback: /* istanbul ignore next */ fn =>
(proc._handler = fn),
once: /* istanbul ignore next */ (_ev, _fn) => proc,
on: /* istanbul ignore next */ (_ev, _fn) => proc,
removeListener: /* istanbul ignore next */ _fn => proc,
}
const debug = proc.env.ASYNC_HOOK_DOMAIN_DEBUG !== '1' ? () => {}
: (() => {
const {writeSync} = require('fs')
const {format} = require('util')
const debugAlways = (() => {
const { writeSync } = require('fs')
const { format } = require('util')
return (...args) => writeSync(2, format(...args) + '\n')
})()
const debug = proc.env.ASYNC_HOOK_DOMAIN_DEBUG !== '1' ? () => {} : debugAlways
const domains = new Map()
// this is to work around the fact that node loses the executionAsyncId
// when a Promise rejects within an async context, for some reason.
// See: https://github.com/nodejs/node/issues/26794
let promiseExecutionId = null
let activePromise = null
// possible values here:
// throw (default)
// we let our rejection handler call the domain handler
// none, warn-with-error-code
// same as default
// warn
// same as default (no way to make it any less noisy, sadly)
// strict
// set the uncaughtExceptionMonitor, because it will throw,
// but do NOT set our rejection handler, or it'll double-handle
const unhandledRejectionMode = (() => {
let mode = 'throw'
for (let i = 0; i < proc.execArgv.length; i++) {
const m = process.execArgv[i]
if (m.startsWith('--unhandled-rejections=')) {
mode = m.substring('--unhandled-rejections='.length)
} else if (m === '--unhandled-rejections') {
mode = proc.execArgv[i + 1]
}
}
return mode
})()

@@ -36,4 +62,6 @@ // the async hook activation and deactivation

domainHook.enable()
proc.emit = domainProcessEmit
proc._fatalException = domainProcessFatalException
proc.on('uncaughtExceptionMonitor', domainErrorHandler)
if (unhandledRejectionMode !== 'strict') {
proc.emit = domainProcessEmit
}
}

@@ -46,13 +74,74 @@ }

domainHook = null
proc.emit = realProcessEmit
proc._fatalException = realProcessFatalException
proc.removeListener('uncaughtExceptionMonitor', domainErrorHandler)
proc.emit = originalProcessEmit
}
}
// monkey patch to silently listen on unhandledRejection, without
// marking the event as 'handled' unless we handled it.
// Do nothing if there's a user handler for the event, though.
const originalProcessEmit = proc.emit
const domainProcessEmit = (ev, ...args) => {
if (
ev !== 'unhandledRejection' ||
proc.listeners('unhandledRejection').length
) {
return originalProcessEmit.call(proc, ev, ...args)
}
const er = args[0]
return domainErrorHandler(er, 'unhandledRejection', true)
}
const domainErrorHandler = (er, ev, rejectionHandler = false) => {
debug('AHD MAYBE HANDLE?', ev, er)
// if anything else attached a handler, then it's their problem,
// not ours. get out of the way.
if (
proc.hasUncaughtExceptionCaptureCallback() ||
proc.listeners('uncaughtException').length > 0
) {
debug('OTHER HANDLER ALREADY SET')
return false
}
const domain = currentDomain()
if (domain) {
debug('HAVE DOMAIN')
try {
domain.onerror(er, ev)
} catch (e) {
debug('ONERROR THREW', e)
domain.destroy()
// this is pretty bad. treat it as a fatal exception, which
// may or may not be caught in the next domain up.
// We drop 'from promise', because now it's a throw.
if (domainErrorHandler(e)) {
return true
}
throw e
}
// at this point, we presumably handled the error, and attach a
// no-op one-time handler to just prevent the crash from happening.
if (!rejectionHandler) {
proc.setUncaughtExceptionCaptureCallback(() => {
debug('UECC ONCE')
proc.setUncaughtExceptionCaptureCallback(null)
})
// in strict mode, node raises the error *before* the uR event,
// and it warns if the uR event is not handled.
if (unhandledRejectionMode === 'strict') {
process.once('unhandledRejection', () => {})
}
}
return true
}
return false
}
// the hook callbacks
const hookMethods = {
init (id, type, triggerId, resource) {
init(id, type, triggerId) {
debug('INIT', id, type, triggerId)
const current = domains.get(triggerId)
if (current) {
debug('INIT', id, type, current)
debug('INIT have current', current)
current.ids.add(id)

@@ -64,122 +153,21 @@ domains.set(id, current)

promiseResolve (id) {
debug('PROMISE RESOLVE', id)
promiseExecutionId = id
},
destroy (id) {
destroy(id) {
const domain = domains.get(id)
debug('DESTROY', id, domain && domain.ids)
if (!domain)
debug('DESTROY', id)
if (!domain) {
return
}
domains.delete(id)
domain.ids.delete(id)
if (!domain.ids.size)
if (!domain.ids.size) {
domain.destroy()
}
},
}
// Dangerous monkey-patching ahead.
// Errors can bubble up to the top level in one of two ways:
// 1. thrown
// 2. promise rejection
//
// Thrown errors are easy. They emit `uncaughtException`, and
// are considered nonfatal if there are listeners that don't throw.
// Managing an event listener is relatively straightforward, but we
// need to recognize when the error ISN'T handled by a domain, and
// make the error fatal, which is tricky but doable.
//
// Promise rejections are harder. They do one of four possible things,
// depending on the --unhandled-rejections argument passed to node.
// - throw:
// - call process._fatalException(er) and THEN emits unhandledRejection
// - emit unhandledRejection
// - if no handlers, warn
// - ignore: emit only
// - always warn: emit event, then warn
// - default:
// - emit event
// - if not handled, print warning and deprecation
//
// When we're ready to make a hard break with the domains builtin, and
// drop support for everything below 12.11.0, it'd be good to do this with
// a process.setUncaughtExceptionCaptureCallback(). However, the downside
// there is that any module that does this has to be a singleton, which
// feels overly pushy for a library like this.
//
// Also, there's been changes in how this all works between v8 and now.
//
// To cover all cases, we monkey-patch process._fatalException and .emit
const currentDomain = () => domains.get(executionAsyncId())
const _handled = Symbol('handled by async-hook-domain')
const domainProcessEmit = (ev, ...args) => {
if (ev === 'unhandledRejection') {
debug('DOMAIN PROCESS EMIT', ev, ...args)
const er = args[0]
const p = args[1]
// check to see if we have a domain
const fromPromise = ev === 'unhandledRejection'
const domain = currentDomain(fromPromise)
if (domain) {
debug('HAS DOMAIN', domain)
if (promiseFatal) {
// don't need to handle a second time when the event emits
return realProcessEmit.call(proc, ev, ...args) || true
}
try {
domain.onerror(er, ev)
} catch (e) {
domain.destroy()
// this is pretty bad. treat it as a fatal exception, which
// may or may not be caught in the next domain up.
// We drop 'from promise', because now it's a throw.
return domainProcessFatalException(e)
}
return realProcessEmit.call(proc, ev, ...args) || true
}
}
return realProcessEmit.call(proc, ev, ...args)
}
const currentDomain = fromPromise =>
domains.get(executionAsyncId()) ||
(fromPromise ? domains.get(promiseExecutionId) : null)
const realProcessEmit = proc.emit
let promiseFatal = false
const domainProcessFatalException = (er, fromPromise) => {
debug('_FATAL EXCEPTION', er, fromPromise)
const domain = currentDomain(fromPromise)
if (domain) {
const ev = fromPromise ? 'unhandledRejection' : 'uncaughtException'
// if it's from a promise, then that means --unhandled-rejection=strict
// we don't need to handle it a second time.
promiseFatal = promiseFatal || fromPromise
try {
domain.onerror(er, ev)
} catch (e) {
domain.destroy()
return domainProcessFatalException(e)
}
// we add a handler just to ensure that node knows the event will
// be handled. otherwise we get async hook stack corruption.
if (promiseFatal) {
// don't blow up our process on a promise if we handled it.
return true
}
proc.once(ev, () => {})
// this ||true is just a safety guard. it should always be true.
return realProcessFatalException.call(proc, er, fromPromise) ||
/* istanbul ignore next */ true
}
return realProcessFatalException.call(proc, er, fromPromise)
}
const realProcessFatalException = proc._fatalException
let id = 1
class Domain {
constructor (onerror) {
constructor(onerror) {
if (typeof onerror !== 'function') {

@@ -192,13 +180,18 @@ // point at where the wrong thing was actually done

const eid = executionAsyncId()
this.eid = eid
this.id = id++
this.ids = new Set([eid])
this.onerror = onerror
this.parent = domains.get(executionAsyncId())
this.parent = domains.get(eid)
this.destroyed = false
domains.set(eid, this)
debug('NEW DOMAIN', this.id, this.eid, this.ids)
activateDomains()
}
destroy () {
if (this.destroyed)
destroy() {
if (this.destroyed) {
return
}
debug('DESTROY DOMAIN', this.id, this.eid, this.ids)
this.destroyed = true

@@ -222,4 +215,5 @@ // find the nearest non-destroyed parent, assign all ids to it

this.ids = new Set()
if (!domains.size)
if (!domains.size) {
deactivateDomains()
}
}

@@ -226,0 +220,0 @@ }

{
"name": "async-hook-domain",
"version": "2.0.4",
"version": "3.0.0",
"description": "An implementation of Domain-like error handling, built on async_hooks",
"main": "index.js",
"directories": {
"test": "test"
},
"devDependencies": {
"source-map-support": "^0.5.16",
"tap": "^15.0.9"
"@types/node": "^18.11.9",
"eslint-config-prettier": "^8.5.0",
"prettier": "^2.7.1",
"source-map-support": "^0.5.21",
"tap": "^16.3.1"
},
"scripts": {
"format": "prettier --write . --loglevel warn",
"test": "tap test/fixtures",

@@ -20,2 +21,17 @@ "snap": "tap test/fixtures",

},
"eslintIgnore": [
"/node_modules",
"/tap-snapshots"
],
"prettier": {
"semi": false,
"printWidth": 80,
"tabWidth": 2,
"useTabs": false,
"singleQuote": true,
"jsxSingleQuote": false,
"bracketSameLine": true,
"arrowParens": "avoid",
"endOfLine": "lf"
},
"tap": {

@@ -52,4 +68,4 @@ "node-arg": [

"engines": {
"node": ">=10"
"node": ">=16"
}
}
# async-hook-domain
An implementation of the error-handling properties of the (deprecated) `domain`
node core module, re-implemented on top of
An implementation of the error-handling properties of the
deprecated `domain` node core module, re-implemented on top of
[`async_hooks`](https://nodejs.org/api/async_hooks.html).
[![Linux Build](https://travis-ci.org/tapjs/async-hook-domain.svg?branch=master)](https://travis-ci.org/tapjs/async-hook-domain)
## USAGE

@@ -38,8 +36,6 @@

fs.readFile('does not exist', (er, data) => {
if (er)
throw er
if (er) throw er
})
fs.readFile('also does not exist', (er, data) => {
if (er)
throw er
if (er) throw er
})

@@ -76,8 +72,121 @@ })

If you want to limit a Domain to a narrower scope, you can use node's
If you want to limit a Domain to a narrower scope, you can use
node's
[`AsyncResource`](https://nodejs.org/api/async_hooks.html#async_hooks_class_asyncresource)
class, and instantiate the domain within its `runInAsyncScope(cb)` method.
From then on, the domain will only be active when running from that Async
Resource's scope.
class, and instantiate the domain within its
`runInAsyncScope(cb)` method. From then on, the domain will only
be active when running from that Async Resource's scope.
## Important `new Promise()` Construction Method Caveat
If you create a domain within a `Promise` construction method,
then rejections of that promise will only be handled by the
domain that was active when the Promise constructor was
instantiated, and _not_ the new domain you create within the
constructor.
This is because, even though the _rejection_ happens later, and
any throws are deferred until that time, the Promise construction
method _itself_ is run synchronously. So, the
`executionAsyncId()` in that context is still the same as it was
when the Promise constructor was initiated.
For example:
```js
const Domain = require('async-hook-domain')
const d1 = new Domain(() => console.log('handled by d1'))
new Promise((_, reject) => { // <-- Promise bound to d1 domain
// executionAsyncId identical to outside the Promise constructor
// domains created later have no effect, Promise already bound,
// as it was created at the instant of calling new Promise()
// this is actually a new domain handling any subsequent throws
// in the *parent* context! confusing!
const d2 = new Domain(() => console.log('handled by d2'))
// timeout created in d2's context, *sibling* of eventual
// promise resolution/rejection
setTimeout(() => {
// d3 created as child of d2, but nothing bound to it
// would handle any new async behaviors triggered by
// the setTimeout's async context
const d3 = new Domain(() => console.log('handled by d3'))
// rejection occurs in child context, triggered by
// execution context where new Promise was initiated.
reject(new Error('will be handled by d1!'))
})
})
```
Since Promise construction happens synchronously in the same
`executionAsyncId()` contex as outside the function, domains
created within that context are as if they were created outside
of the Promise constructor, and will stack up for that context.
For example:
```js
const Domain = require('async-hook-domain')
// executionAsyncId=1, domain added
const d1 = new Domain(() => console.log('handled by d1'))
new Promise((_, reject) => {
// still executionAsyncId=1, new child domain takes over
// this is the new active domain for executionAsyncId=1,
// even outside the Promise constructor!
const d2 = new Domain(() => console.log('handled by d2'))
// setTimeout creates new executionAsyncId=3, bound to d2
setTimeout(() => {
// executionAsyncId=3, d3 handling any errors in it
const d3 = new Domain(() => console.log('handled by d3'))
// resolve happens in executionAsyncId=2, the promise
// resolution context triggered by the new Promise call
resolve('this is fine')
})
})
// throw happens in executionAsyncId=1, current domain is d2!
throw new Error('will be handled by d2!')
```
Note that since a throw within a `Promise` construction method is
treated as a call to `reject()`, this also applies to thrown
errors within the construction method:
```js
const Domain = require('async-hook-domain')
const d1 = new Domain(() => console.error('handled by d1'))
new Promise((_, reject) => {
const d2 = new Domain(() => console.error('handled by d2'))
throw 'this will be handled by d1, not d2!'
})
```
The execution context of the Promise itself is bound to the
domain that was active at the time the Promise constructor
_started_, so any rejection will be handled by that domain.
If this all sounds confusing and very deep in the weeds, a safe
approach is to never create a new `Domain` within a Promise
construction function. Then everything will behave as you'd
expect.
I have explored the space here thoroughly, because this strikes
me as counter-intuitive. As a user, I'd expect that a new domain
created in a Promise constructor method would be a child of the
domain that binds _to the Promise resolution_, and thus take over
handling the subsequent Promise rejection, rather than binding to
the context outside the Promise constructor.
But that isn't how it works, and as of version 19, Node.js and v8
do not provide adequate API surface to make it behave that way
without making _other_ behavior less reliable. A future
SemVer-major change will address this caveat when and if it
becomes possible to do so.
## API

@@ -87,29 +196,36 @@

Set the `ASYNC_HOOK_DOMAIN_DEBUG` environment variable to `'1'` to print a lot
of debugging information to stderr.
Set the `ASYNC_HOOK_DOMAIN_DEBUG` environment variable to `'1'`
to print a lot of debugging information to stderr.
### const d = new Domain(errorHandlerFunction(error, type))
Create a new Domain and assign it to the current execution context and all
child contexts that the current one triggers.
Create a new Domain and assign it to the current execution
context and all child contexts that the current one triggers.
The handler function is called with two arguments. The first is the error that
was thrown or the rejection value of the rejected Promise. The second is
either `'uncaughtException'` or `'unhandledRejection'`, depending on the type
of event that raised the error.
The handler function is called with two arguments. The first is
the error that was thrown or the rejection value of the rejected
Promise. The second is either `'uncaughtException'` or
`'unhandledRejection'`, depending on the type of event that
raised the error.
Note that even if the Domain prevents the process from failing
entirely, Node.js _may_ still print a warning about unhandled
rejections, depending on the `--unhandled-rejections` option.
### d.parent Domain
If a Domain is already assigned to the current context on creation, then the
current Domain set as the new Domain's `parent`. On destruction, any of a
Domain's still-active execution contexts are assigned to its parent.
If a Domain is already assigned to the current context on
creation, then the current Domain set as the new Domain's
`parent`. On destruction, any of a Domain's still-active
execution contexts are assigned to its parent.
### d.onerror Function
The `errorHandlerFunction` passed into the constructor. Called when an
uncaughtException or unhandledRejection occurs in the scope of the Domain.
The `errorHandlerFunction` passed into the constructor. Called
when an uncaughtException or unhandledRejection occurs in the
scope of the Domain.
If this function throws, then the domain will be destroyed, and the thrown
error will be raised. If the domain doesn't have a parent, then this will
likely crash the process entirely.
If this function throws, then the domain will be destroyed, and
the thrown error will be raised. If the domain doesn't have a
parent, then this will likely crash the process entirely.

@@ -122,12 +238,13 @@ ### d.destroyed Boolean

A set of the `executionAsyncId` values corresponding to the execution contexts
for which this Domain handles errors.
A set of the `executionAsyncId` values corresponding to the
execution contexts for which this Domain handles errors.
### d.destroy() Function
Call to destroy the domain. This removes it from the system entirely,
assigning any outstanding ids to its parent, if it has one, or leaving them
uncovered if not.
Call to destroy the domain. This removes it from the system
entirely, assigning any outstanding ids to its parent, if it has
one, or leaving them uncovered if not.
This is called implicitly when the domain's last covered execution context is
destroyed, since at that point, the domain is unreachable anyway.
This is called implicitly when the domain's last covered
execution context is destroyed, since at that point, the domain
is unreachable anyway.

Sorry, the diff of this file is not supported yet

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc