Skip to content

Seguimiento de contexto asíncrono

[Estable: 2 - Estable]

Estable: 2 Estabilidad: 2 - Estable

Código fuente: lib/async_hooks.js

Introducción

Estas clases se utilizan para asociar el estado y propagarlo a través de las devoluciones de llamada y las cadenas de promesas. Permiten almacenar datos durante la vida útil de una solicitud web o cualquier otra duración asíncrona. Es similar al almacenamiento local de subprocesos en otros lenguajes.

Las clases AsyncLocalStorage y AsyncResource forman parte del módulo node:async_hooks:

js
import { AsyncLocalStorage, AsyncResource } from 'node:async_hooks'
js
const { AsyncLocalStorage, AsyncResource } = require('node:async_hooks')

Clase: AsyncLocalStorage

[Historial]

VersiónCambios
v16.4.0AsyncLocalStorage ahora es Estable. Anteriormente, había sido Experimental.
v13.10.0, v12.17.0Añadido en: v13.10.0, v12.17.0

Esta clase crea almacenes que permanecen coherentes a través de las operaciones asíncronas.

Si bien puede crear su propia implementación sobre el módulo node:async_hooks, se debe preferir AsyncLocalStorage ya que es una implementación eficiente y segura en memoria que implica optimizaciones significativas que no son obvias de implementar.

El siguiente ejemplo utiliza AsyncLocalStorage para construir un registrador simple que asigna ID a las solicitudes HTTP entrantes y las incluye en los mensajes registrados dentro de cada solicitud.

js
import http from 'node:http'
import { AsyncLocalStorage } from 'node:async_hooks'

const asyncLocalStorage = new AsyncLocalStorage()

function logWithId(msg) {
  const id = asyncLocalStorage.getStore()
  console.log(`${id !== undefined ? id : '-'}:`, msg)
}

let idSeq = 0
http
  .createServer((req, res) => {
    asyncLocalStorage.run(idSeq++, () => {
      logWithId('start')
      // Imagine any chain of async operations here
      setImmediate(() => {
        logWithId('finish')
        res.end()
      })
    })
  })
  .listen(8080)

http.get('http://localhost:8080')
http.get('http://localhost:8080')
// Prints:
//   0: start
//   1: start
//   0: finish
//   1: finish
js
const http = require('node:http')
const { AsyncLocalStorage } = require('node:async_hooks')

const asyncLocalStorage = new AsyncLocalStorage()

function logWithId(msg) {
  const id = asyncLocalStorage.getStore()
  console.log(`${id !== undefined ? id : '-'}:`, msg)
}

let idSeq = 0
http
  .createServer((req, res) => {
    asyncLocalStorage.run(idSeq++, () => {
      logWithId('start')
      // Imagine any chain of async operations here
      setImmediate(() => {
        logWithId('finish')
        res.end()
      })
    })
  })
  .listen(8080)

http.get('http://localhost:8080')
http.get('http://localhost:8080')
// Prints:
//   0: start
//   1: start
//   0: finish
//   1: finish

Cada instancia de AsyncLocalStorage mantiene un contexto de almacenamiento independiente. Pueden existir múltiples instancias simultáneamente sin riesgo de interferir con los datos de cada una.

new AsyncLocalStorage()

[Historial]

VersiónCambios
v19.7.0, v18.16.0Se eliminó la opción experimental onPropagate.
v19.2.0, v18.13.0Se agregó la opción onPropagate.
v13.10.0, v12.17.0Añadido en: v13.10.0, v12.17.0

Crea una nueva instancia de AsyncLocalStorage. El almacenamiento solo se proporciona dentro de una llamada run() o después de una llamada enterWith().

Método estático: AsyncLocalStorage.bind(fn)

Añadido en: v19.8.0, v18.16.0

[Estable: 1 - Experimental]

Estable: 1 Estabilidad: 1 - Experimental

  • fn <Función> La función que se enlazará al contexto de ejecución actual.
  • Devuelve: <Función> Una nueva función que llama a fn dentro del contexto de ejecución capturado.

Enlaza la función dada al contexto de ejecución actual.

Método estático: AsyncLocalStorage.snapshot()

Añadido en: v19.8.0, v18.16.0

[Estable: 1 - Experimental]

Estable: 1 Estabilidad: 1 - Experimental

  • Devuelve: <Function> Una nueva función con la firma (fn: (...args) : R, ...args) : R.

Captura el contexto de ejecución actual y devuelve una función que acepta una función como argumento. Siempre que se llama a la función devuelta, llama a la función que se le pasa dentro del contexto capturado.

js
const asyncLocalStorage = new AsyncLocalStorage()
const runInAsyncScope = asyncLocalStorage.run(123, () => AsyncLocalStorage.snapshot())
const result = asyncLocalStorage.run(321, () => runInAsyncScope(() => asyncLocalStorage.getStore()))
console.log(result) // devuelve 123

AsyncLocalStorage.snapshot() puede reemplazar el uso de AsyncResource para propósitos simples de seguimiento de contexto asincrónico, por ejemplo:

js
class Foo {
  #runInAsyncScope = AsyncLocalStorage.snapshot()

  get() {
    return this.#runInAsyncScope(() => asyncLocalStorage.getStore())
  }
}

const foo = asyncLocalStorage.run(123, () => new Foo())
console.log(asyncLocalStorage.run(321, () => foo.get())) // devuelve 123

asyncLocalStorage.disable()

Añadido en: v13.10.0, v12.17.0

[Estable: 1 - Experimental]

Estable: 1 Estabilidad: 1 - Experimental

Desactiva la instancia de AsyncLocalStorage. Todas las llamadas posteriores a asyncLocalStorage.getStore() devolverán undefined hasta que se llame de nuevo a asyncLocalStorage.run() o asyncLocalStorage.enterWith().

Al llamar a asyncLocalStorage.disable(), se saldrá de todos los contextos actuales vinculados a la instancia.

Es necesario llamar a asyncLocalStorage.disable() antes de que asyncLocalStorage pueda ser recolectado por el garbage collector. Esto no se aplica a los almacenes proporcionados por asyncLocalStorage, ya que esos objetos se recolectan junto con los recursos asincrónicos correspondientes.

Utilice este método cuando asyncLocalStorage ya no esté en uso en el proceso actual.

asyncLocalStorage.getStore()

Añadido en: v13.10.0, v12.17.0

Devuelve el almacén actual. Si se llama fuera de un contexto asincrónico inicializado llamando a asyncLocalStorage.run() o asyncLocalStorage.enterWith(), devuelve undefined.

asyncLocalStorage.enterWith(store)

Añadido en: v13.11.0, v12.17.0

[Estable: 1 - Experimental]

Estable: 1 Estabilidad: 1 - Experimental

Transiciona al contexto durante el resto de la ejecución síncrona actual y luego persiste el almacén a través de cualquier llamada asíncrona posterior.

Ejemplo:

js
const store = { id: 1 }
// Reemplaza el almacén anterior con el objeto de almacén dado
asyncLocalStorage.enterWith(store)
asyncLocalStorage.getStore() // Devuelve el objeto de almacén
someAsyncOperation(() => {
  asyncLocalStorage.getStore() // Devuelve el mismo objeto
})

Esta transición continuará durante toda la ejecución síncrona. Esto significa que si, por ejemplo, se ingresa al contexto dentro de un controlador de eventos, los controladores de eventos posteriores también se ejecutarán dentro de ese contexto a menos que estén específicamente vinculados a otro contexto con un AsyncResource. Es por eso que se debe preferir run() sobre enterWith() a menos que existan razones de peso para usar el último método.

js
const store = { id: 1 }

emitter.on('my-event', () => {
  asyncLocalStorage.enterWith(store)
})
emitter.on('my-event', () => {
  asyncLocalStorage.getStore() // Devuelve el mismo objeto
})

asyncLocalStorage.getStore() // Devuelve indefinido
emitter.emit('my-event')
asyncLocalStorage.getStore() // Devuelve el mismo objeto

asyncLocalStorage.run(store, callback[, ...args])

Añadido en: v13.10.0, v12.17.0

Ejecuta una función sincrónicamente dentro de un contexto y devuelve su valor de retorno. El almacén no es accesible fuera de la función de devolución de llamada. El almacén es accesible para cualquier operación asíncrona creada dentro de la devolución de llamada.

Los argumentos opcionales args se pasan a la función de devolución de llamada.

Si la función de devolución de llamada lanza un error, la función run() también lanza el error. El rastreo de la pila no se ve afectado por esta llamada y se sale del contexto.

Ejemplo:

js
const store = { id: 2 }
try {
  asyncLocalStorage.run(store, () => {
    asyncLocalStorage.getStore() // Devuelve el objeto store
    setTimeout(() => {
      asyncLocalStorage.getStore() // Devuelve el objeto store
    }, 200)
    throw new Error()
  })
} catch (e) {
  asyncLocalStorage.getStore() // Devuelve undefined
  // El error será capturado aquí
}

asyncLocalStorage.exit(callback[, ...args])

Añadido en: v13.10.0, v12.17.0

[Estable: 1 - Experimental]

Estable: 1 Estabilidad: 1 - Experimental

Ejecuta una función sincrónicamente fuera de un contexto y devuelve su valor de retorno. El almacén no es accesible dentro de la función de devolución de llamada o las operaciones asíncronas creadas dentro de la devolución de llamada. Cualquier llamada a getStore() realizada dentro de la función de devolución de llamada siempre devolverá undefined.

Los argumentos opcionales args se pasan a la función de devolución de llamada.

Si la función de devolución de llamada lanza un error, la función exit() también lanza el error. El rastreo de pila no se ve afectado por esta llamada y se vuelve a entrar en el contexto.

Ejemplo:

js
// Dentro de una llamada a run
try {
  asyncLocalStorage.getStore() // Devuelve el objeto o valor del almacén
  asyncLocalStorage.exit(() => {
    asyncLocalStorage.getStore() // Devuelve undefined
    throw new Error()
  })
} catch (e) {
  asyncLocalStorage.getStore() // Devuelve el mismo objeto o valor
  // El error será capturado aquí
}

Uso con async/await

Si, dentro de una función async, solo se debe ejecutar una llamada await dentro de un contexto, se debe usar el siguiente patrón:

js
async function fn() {
  await asyncLocalStorage.run(new Map(), () => {
    asyncLocalStorage.getStore().set('key', value)
    return foo() // El valor de retorno de foo será esperado
  })
}

En este ejemplo, el almacén solo está disponible en la función de devolución de llamada y las funciones llamadas por foo. Fuera de run, llamar a getStore devolverá undefined.

Solución de problemas: Pérdida de contexto

En la mayoría de los casos, AsyncLocalStorage funciona sin problemas. En situaciones excepcionales, el almacén actual se pierde en una de las operaciones asíncronas.

Si su código está basado en devoluciones de llamada, basta con convertirlo en promesas con util.promisify() para que empiece a funcionar con promesas nativas.

Si necesita usar una API basada en devoluciones de llamada o su código asume una implementación thenable personalizada, use la clase AsyncResource para asociar la operación asíncrona con el contexto de ejecución correcto. Encuentre la llamada a la función responsable de la pérdida de contexto registrando el contenido de asyncLocalStorage.getStore() después de las llamadas que sospecha que son responsables de la pérdida. Cuando el código registre undefined, la última devolución de llamada llamada es probablemente la responsable de la pérdida de contexto.

Clase: AsyncResource

[Historial]

VersiónCambios
v16.4.0AsyncResource ahora es Estable. Anteriormente, había sido Experimental.

La clase AsyncResource está diseñada para ser extendida por los recursos asincrónicos del integrador. Usando esto, los usuarios pueden fácilmente activar los eventos de ciclo de vida de sus propios recursos.

El gancho init se activará cuando se cree una instancia de AsyncResource.

A continuación se muestra una descripción general de la API de AsyncResource.

js
import { AsyncResource, executionAsyncId } from 'node:async_hooks'

// AsyncResource() está diseñado para ser extendido. Instanciar un
// nuevo AsyncResource() también activa init. Si se omite triggerAsyncId, entonces
// async_hook.executionAsyncId() es usado.
const asyncResource = new AsyncResource(type, { triggerAsyncId: executionAsyncId(), requireManualDestroy: false })

// Ejecuta una función en el contexto de ejecución del recurso. Esto hará
// * establecer el contexto del recurso
// * activar las devoluciones de llamada de AsyncHooks antes
// * llamar a la función proporcionada `fn` con los argumentos suministrados
// * activar las devoluciones de llamada de AsyncHooks después
// * restaurar el contexto de ejecución original
asyncResource.runInAsyncScope(fn, thisArg, ...args)

// Llamar a las devoluciones de llamada de destrucción de AsyncHooks.
asyncResource.emitDestroy()

// Devuelve el ID único asignado a la instancia de AsyncResource.
asyncResource.asyncId()

// Devuelve el ID de activación para la instancia de AsyncResource.
asyncResource.triggerAsyncId()
js
const { AsyncResource, executionAsyncId } = require('node:async_hooks')

// AsyncResource() está diseñado para ser extendido. Instanciar un
// nuevo AsyncResource() también activa init. Si se omite triggerAsyncId, entonces
// async_hook.executionAsyncId() es usado.
const asyncResource = new AsyncResource(type, { triggerAsyncId: executionAsyncId(), requireManualDestroy: false })

// Ejecuta una función en el contexto de ejecución del recurso. Esto hará
// * establecer el contexto del recurso
// * activar las devoluciones de llamada de AsyncHooks antes
// * llamar a la función proporcionada `fn` con los argumentos suministrados
// * activar las devoluciones de llamada de AsyncHooks después
// * restaurar el contexto de ejecución original
asyncResource.runInAsyncScope(fn, thisArg, ...args)

// Llamar a las devoluciones de llamada de destrucción de AsyncHooks.
asyncResource.emitDestroy()

// Devuelve el ID único asignado a la instancia de AsyncResource.
asyncResource.asyncId()

// Devuelve el ID de activación para la instancia de AsyncResource.
asyncResource.triggerAsyncId()

new AsyncResource(type[, options])

  • type <string> El tipo de evento asincrónico.
  • options <Object>
    • triggerAsyncId <number> El ID del contexto de ejecución que creó este evento asincrónico. Predeterminado: executionAsyncId().
    • requireManualDestroy <boolean> Si se establece en true, deshabilita emitDestroy cuando el objeto se recoge como basura. Normalmente no es necesario establecerlo (incluso si emitDestroy se llama manualmente), a menos que se recupere el asyncId del recurso y se llame a emitDestroy de las API sensibles con él. Cuando se establece en false, la llamada emitDestroy en la recolección de basura solo tendrá lugar si hay al menos un gancho destroy activo. Predeterminado: false.

Ejemplo de uso:

js
class DBQuery extends AsyncResource {
  constructor(db) {
    super('DBQuery')
    this.db = db
  }

  getInfo(query, callback) {
    this.db.get(query, (err, data) => {
      this.runInAsyncScope(callback, null, err, data)
    })
  }

  close() {
    this.db = null
    this.emitDestroy()
  }
}

Método estático: AsyncResource.bind(fn[, type[, thisArg]])

[Historial]

VersiónCambios
v20.0.0La propiedad asyncResource agregada a la función enlazada ha quedado en desuso y se eliminará en una versión futura.
v17.8.0, v16.15.0Se cambió el valor predeterminado cuando thisArg es indefinido para usar this del llamador.
v16.0.0Se agregó thisArg opcional.
v14.8.0, v12.19.0Añadido en: v14.8.0, v12.19.0
  • fn <Function> La función que se enlazará al contexto de ejecución actual.
  • type <string> Un nombre opcional para asociar con el AsyncResource subyacente.
  • thisArg <any>

Enlaza la función dada al contexto de ejecución actual.

asyncResource.bind(fn[, thisArg])

[Historial]

VersiónCambios
v20.0.0La propiedad asyncResource agregada a la función enlazada ha quedado en desuso y se eliminará en una versión futura.
v17.8.0, v16.15.0Se cambió el valor predeterminado cuando thisArg es indefinido para usar this del llamador.
v16.0.0Se agregó thisArg opcional.
v14.8.0, v12.19.0Añadido en: v14.8.0, v12.19.0
  • fn <Function> La función que se enlazará para ejecutar en el ámbito de este AsyncResource.
  • thisArg <any>

Enlaza la función dada para que se ejecute en el ámbito de este AsyncResource.

asyncResource.runInAsyncScope(fn[, thisArg, ...args])

Añadido en: v9.6.0

  • fn <Function> La función que se llamará en el contexto de ejecución de este recurso asincrónico.
  • thisArg <any> El receptor que se utilizará para la llamada a la función.
  • ...args <any> Argumentos opcionales que se pasarán a la función.

Llama a la función proporcionada con los argumentos proporcionados en el contexto de ejecución del recurso asincrónico. Esto establecerá el contexto, activará los AsyncHooks antes de las devoluciones de llamada, llamará a la función, activará los AsyncHooks después de las devoluciones de llamada y luego restaurará el contexto de ejecución original.

asyncResource.emitDestroy()

Llama a todos los ganchos destroy. Esto solo debe llamarse una vez. Se lanzará un error si se llama más de una vez. Esto debe llamarse manualmente. Si se deja que el recurso sea recolectado por el GC, entonces los ganchos destroy nunca se llamarán.

asyncResource.asyncId()

  • Devuelve: <number> El asyncId único asignado al recurso.

asyncResource.triggerAsyncId()

  • Devuelve: <number> El mismo triggerAsyncId que se pasa al constructor AsyncResource.

Uso de AsyncResource para un grupo de subprocesos Worker

El siguiente ejemplo muestra cómo usar la clase AsyncResource para proporcionar correctamente el seguimiento asincrónico de un grupo de Worker. Otros grupos de recursos, como los grupos de conexiones a bases de datos, pueden seguir un modelo similar.

Suponiendo que la tarea es sumar dos números, usando un archivo llamado task_processor.js con el siguiente contenido:

js
import { parentPort } from 'node:worker_threads'
parentPort.on('message', task => {
  parentPort.postMessage(task.a + task.b)
})
js
const { parentPort } = require('node:worker_threads')
parentPort.on('message', task => {
  parentPort.postMessage(task.a + task.b)
})

un grupo de Worker alrededor de él podría usar la siguiente estructura:

js
import { AsyncResource } from 'node:async_hooks'
import { EventEmitter } from 'node:events'
import { Worker } from 'node:worker_threads'

const kTaskInfo = Symbol('kTaskInfo')
const kWorkerFreedEvent = Symbol('kWorkerFreedEvent')

class WorkerPoolTaskInfo extends AsyncResource {
  constructor(callback) {
    super('WorkerPoolTaskInfo')
    this.callback = callback
  }

  done(err, result) {
    this.runInAsyncScope(this.callback, null, err, result)
    this.emitDestroy() // `TaskInfo`s are used only once.
  }
}

export default class WorkerPool extends EventEmitter {
  constructor(numThreads) {
    super()
    this.numThreads = numThreads
    this.workers = []
    this.freeWorkers = []
    this.tasks = []

    for (let i = 0; i < numThreads; i++) this.addNewWorker()

    // Any time the kWorkerFreedEvent is emitted, dispatch
    // the next task pending in the queue, if any.
    this.on(kWorkerFreedEvent, () => {
      if (this.tasks.length > 0) {
        const { task, callback } = this.tasks.shift()
        this.runTask(task, callback)
      }
    })
  }

  addNewWorker() {
    const worker = new Worker(new URL('task_processor.js', import.meta.url))
    worker.on('message', result => {
      // In case of success: Call the callback that was passed to `runTask`,
      // remove the `TaskInfo` associated with the Worker, and mark it as free
      // again.
      worker[kTaskInfo].done(null, result)
      worker[kTaskInfo] = null
      this.freeWorkers.push(worker)
      this.emit(kWorkerFreedEvent)
    })
    worker.on('error', err => {
      // In case of an uncaught exception: Call the callback that was passed to
      // `runTask` with the error.
      if (worker[kTaskInfo]) worker[kTaskInfo].done(err, null)
      else this.emit('error', err)
      // Remove the worker from the list and start a new Worker to replace the
      // current one.
      this.workers.splice(this.workers.indexOf(worker), 1)
      this.addNewWorker()
    })
    this.workers.push(worker)
    this.freeWorkers.push(worker)
    this.emit(kWorkerFreedEvent)
  }

  runTask(task, callback) {
    if (this.freeWorkers.length === 0) {
      // No free threads, wait until a worker thread becomes free.
      this.tasks.push({ task, callback })
      return
    }

    const worker = this.freeWorkers.pop()
    worker[kTaskInfo] = new WorkerPoolTaskInfo(callback)
    worker.postMessage(task)
  }

  close() {
    for (const worker of this.workers) worker.terminate()
  }
}
js
const { AsyncResource } = require('node:async_hooks')
const { EventEmitter } = require('node:events')
const path = require('node:path')
const { Worker } = require('node:worker_threads')

const kTaskInfo = Symbol('kTaskInfo')
const kWorkerFreedEvent = Symbol('kWorkerFreedEvent')

class WorkerPoolTaskInfo extends AsyncResource {
  constructor(callback) {
    super('WorkerPoolTaskInfo')
    this.callback = callback
  }

  done(err, result) {
    this.runInAsyncScope(this.callback, null, err, result)
    this.emitDestroy() // `TaskInfo`s are used only once.
  }
}

class WorkerPool extends EventEmitter {
  constructor(numThreads) {
    super()
    this.numThreads = numThreads
    this.workers = []
    this.freeWorkers = []
    this.tasks = []

    for (let i = 0; i < numThreads; i++) this.addNewWorker()

    // Any time the kWorkerFreedEvent is emitted, dispatch
    // the next task pending in the queue, if any.
    this.on(kWorkerFreedEvent, () => {
      if (this.tasks.length > 0) {
        const { task, callback } = this.tasks.shift()
        this.runTask(task, callback)
      }
    })
  }

  addNewWorker() {
    const worker = new Worker(path.resolve(__dirname, 'task_processor.js'))
    worker.on('message', result => {
      // In case of success: Call the callback that was passed to `runTask`,
      // remove the `TaskInfo` associated with the Worker, and mark it as free
      // again.
      worker[kTaskInfo].done(null, result)
      worker[kTaskInfo] = null
      this.freeWorkers.push(worker)
      this.emit(kWorkerFreedEvent)
    })
    worker.on('error', err => {
      // In case of an uncaught exception: Call the callback that was passed to
      // `runTask` with the error.
      if (worker[kTaskInfo]) worker[kTaskInfo].done(err, null)
      else this.emit('error', err)
      // Remove the worker from the list and start a new Worker to replace the
      // current one.
      this.workers.splice(this.workers.indexOf(worker), 1)
      this.addNewWorker()
    })
    this.workers.push(worker)
    this.freeWorkers.push(worker)
    this.emit(kWorkerFreedEvent)
  }

  runTask(task, callback) {
    if (this.freeWorkers.length === 0) {
      // No free threads, wait until a worker thread becomes free.
      this.tasks.push({ task, callback })
      return
    }

    const worker = this.freeWorkers.pop()
    worker[kTaskInfo] = new WorkerPoolTaskInfo(callback)
    worker.postMessage(task)
  }

  close() {
    for (const worker of this.workers) worker.terminate()
  }
}

module.exports = WorkerPool

Sin el seguimiento explícito agregado por los objetos WorkerPoolTaskInfo, parecería que las funciones de devolución de llamada están asociadas con los objetos Worker individuales. Sin embargo, la creación de los Worker no está asociada con la creación de las tareas y no proporciona información sobre cuándo se programaron las tareas.

Este grupo podría usarse de la siguiente manera:

js
import WorkerPool from './worker_pool.js'
import os from 'node:os'

const pool = new WorkerPool(os.availableParallelism())

let finished = 0
for (let i = 0; i < 10; i++) {
  pool.runTask({ a: 42, b: 100 }, (err, result) => {
    console.log(i, err, result)
    if (++finished === 10) pool.close()
  })
}
js
const WorkerPool = require('./worker_pool.js')
const os = require('node:os')

const pool = new WorkerPool(os.availableParallelism())

let finished = 0
for (let i = 0; i < 10; i++) {
  pool.runTask({ a: 42, b: 100 }, (err, result) => {
    console.log(i, err, result)
    if (++finished === 10) pool.close()
  })
}

Integrando AsyncResource con EventEmitter

Los listeners de eventos activados por un EventEmitter pueden ejecutarse en un contexto de ejecución diferente al que estaba activo cuando se llamó a eventEmitter.on().

El siguiente ejemplo muestra cómo usar la clase AsyncResource para asociar correctamente un listener de eventos con el contexto de ejecución correcto. El mismo enfoque se puede aplicar a un Stream o una clase similar basada en eventos.

js
import { createServer } from 'node:http'
import { AsyncResource, executionAsyncId } from 'node:async_hooks'

const server = createServer((req, res) => {
  req.on(
    'close',
    AsyncResource.bind(() => {
      // El contexto de ejecución está enlazado al ámbito externo actual.
    })
  )
  req.on('close', () => {
    // El contexto de ejecución está enlazado al ámbito que provocó la emisión de 'close'.
  })
  res.end()
}).listen(3000)
js
const { createServer } = require('node:http')
const { AsyncResource, executionAsyncId } = require('node:async_hooks')

const server = createServer((req, res) => {
  req.on(
    'close',
    AsyncResource.bind(() => {
      // El contexto de ejecución está enlazado al ámbito externo actual.
    })
  )
  req.on('close', () => {
    // El contexto de ejecución está enlazado al ámbito que provocó la emisión de 'close'.
  })
  res.end()
}).listen(3000)