{"id":26718959,"url":"https://github.com/psenger/async-context-id","last_synced_at":"2026-02-14T07:31:21.954Z","repository":{"id":279043966,"uuid":"928072717","full_name":"psenger/async-context-id","owner":"psenger","description":"A lightweight, powerful correlation ID tracking and context store written for Node.js that automatically propagates context through async operations using Node's `async_hooks`. Perfect for distributed tracing, request tracking, and logging correlation in micro services architectures.","archived":false,"fork":false,"pushed_at":"2025-03-26T01:17:28.000Z","size":211,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-10-06T13:54:44.346Z","etag":null,"topics":["async-hooks","asynchooks","context","correlation","correlation-id","correlationid","debug","distributed-tracing","express","logging","microservices","middleware","nodejs","request-id","tracking"],"latest_commit_sha":null,"homepage":"https://psenger.github.io/async-context-id/","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/psenger.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":".github/CODEOWNERS","security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2025-02-06T02:31:35.000Z","updated_at":"2025-03-14T02:32:12.000Z","dependencies_parsed_at":"2025-03-26T02:23:25.380Z","dependency_job_id":"674cec79-27b4-47b3-ab40-3f60bd41ef7a","html_url":"https://github.com/psenger/async-context-id","commit_stats":null,"previous_names":["psenger/async-context-id"],"tags_count":7,"template":false,"template_full_name":null,"purl":"pkg:github/psenger/async-context-id","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/psenger%2Fasync-context-id","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/psenger%2Fasync-context-id/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/psenger%2Fasync-context-id/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/psenger%2Fasync-context-id/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/psenger","download_url":"https://codeload.github.com/psenger/async-context-id/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/psenger%2Fasync-context-id/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29439490,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-02-14T07:24:13.446Z","status":"ssl_error","status_checked_at":"2026-02-14T07:23:58.969Z","response_time":53,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["async-hooks","asynchooks","context","correlation","correlation-id","correlationid","debug","distributed-tracing","express","logging","microservices","middleware","nodejs","request-id","tracking"],"created_at":"2025-03-27T17:50:54.721Z","updated_at":"2026-02-14T07:31:21.935Z","avatar_url":"https://github.com/psenger.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# [async-context-id](https://github.com/psenger/async-context-id#readme)\n\n\u003e [!TAG]\n\u003e 0.1.1\n\nA lightweight, powerful correlation ID tracking and context store written for Node.js that automatically propagates context through async operations using Node's `async_hooks`. Perfect for distributed tracing, request tracking, and logging correlation in micro services architectures.\n\n## Table of Contents\n\n\u003c!-- toc --\u003e\n\n- [Features](#features)\n- [Installation](#installation)\n- [How It Works](#how-it-works)\n- [Flow Diagram Explanation](#flow-diagram-explanation)\n  * [Instance Creation](#instance-creation)\n  * [Async Hooks Setup](#async-hooks-setup)\n  * [Context Operations](#context-operations)\n  * [Context Propagation](#context-propagation)\n  * [Cleanup](#cleanup)\n  * [Example Flow](#example-flow)\n- [API](#api)\n  * [Modules](#modules)\n  * [Classes](#classes)\n  * [@psenger/async-context-id](#psengerasync-context-id)\n  * [AsyncContextId](#asynccontextid)\n    + [new AsyncContextId([options])](#new-asynccontextidoptions)\n    + [asyncContextId.cleanUpFn()](#asynccontextidcleanupfn)\n    + [asyncContextId.getCorrelationId() ⇒ string](#asynccontextidgetcorrelationid-%E2%87%92-string)\n    + [asyncContextId.setCorrelationId(correlationId)](#asynccontextidsetcorrelationidcorrelationid)\n    + [asyncContextId.getContext() ⇒ Object \\| string \\| number \\| Object](#asynccontextidgetcontext-%E2%87%92-object--string--number--object)\n    + [asyncContextId.setContext([context])](#asynccontextidsetcontextcontext)\n    + [asyncContextId.clear()](#asynccontextidclear)\n  * [LruMap ⇐ Map](#lrumap-%E2%87%90-map)\n    + [new LruMap(maxSize)](#new-lrumapmaxsize)\n    + [lruMap.set(key, value) ⇒ this](#lrumapsetkey-value-%E2%87%92-this)\n  * [TimedMap ⇐ Map](#timedmap-%E2%87%90-map)\n    + [new TimedMap(ttl)](#new-timedmapttl)\n    + [timedMap.set(key, value) ⇒ this](#timedmapsetkey-value-%E2%87%92-this)\n    + [timedMap.delete(key) ⇒ boolean](#timedmapdeletekey-%E2%87%92-boolean)\n- [Usage](#usage)\n  * [Enhanced Logging with Console Monkey Patching](#enhanced-logging-with-console-monkey-patching)\n  * [Express Middleware](#express-middleware)\n    + [`correlation-middleware.js`](#correlation-middlewarejs)\n    + [`app.js`](#appjs)\n    + [`simple-controller.js`](#simple-controllerjs)\n  * [Winston Logger Integration](#winston-logger-integration)\n- [‼️ Caution - Memory Management Strategies](#%E2%80%BC%EF%B8%8F-caution---memory-management-strategies)\n- [‼️ Caution - Correlation ID with `UUID`](#%E2%80%BC%EF%B8%8F-caution---correlation-id-with-uuid)\n- [‼️ Limitations](#%E2%80%BC%EF%B8%8F-limitations)\n  * [Untested pattern - **Process Next Tick**](#untested-pattern---process-next-tick)\n  * [Untested pattern - **Node.js Core Module Callbacks** (like fs, http, etc.):](#untested-pattern---nodejs-core-module-callbacks-like-fs-http-etc)\n  * [Solution to Untested pattern](#solution-to-untested-pattern)\n- [Contributing](#contributing)\n  * [Rules](#rules)\n  * [Commit Message](#commit-message)\n  * [Testing](#testing)\n- [License](#license)\n- [Acknowledgments](#acknowledgments)\n  * [Dependencies](#dependencies)\n  * [Development Dependencies](#development-dependencies)\n\n\u003c!-- tocstop --\u003e\n\n## Features\n\n- Automatic context propagation through async operations\n- Thread-local-like storage for Node.js\n- UUID v4 based correlation IDs\n- ️ Thread-safe context isolation\n- Deep cloning of context data\n- Automatic cleanup of contexts\n- Zero dependencies (uses only Node.js built-ins)\n\n\u003c!--START_SECTION:file:INSTALLATION.md--\u003e\n## Installation\n\n**NPM**\n\n```shell\nnpm install @psenger/async-context-id --save\n```\n**YARN**\n\n```shell\nyarn add @psenger/async-context-id\n```\n\n\u003c!--END_SECTION:file:INSTALLATION.md--\u003e\n\n\u003c!--START_SECTION:file:HOWITWORKS.md--\u003e\n## How It Works\n\nThis library uses Node.js's `async_hooks` module to track async operations:\n\n```mermaid\nstateDiagram-v2\n    [*] --\u003e AsyncContextId: Create Instance\n\n    state AsyncContextId {\n        state \"Async Hooks Setup\" as setup {\n            [*] --\u003e Hook\n            Hook --\u003e Init: New Async Operation\n            Hook --\u003e PromiseResolve: Promise Resolved\n            Hook --\u003e Destroy: Operation Complete\n        }\n\n        state \"Context Operations\" as ops {\n            state \"Context Store (Map)\" as store {\n                SetContext --\u003e Store: Update Context\n                GetContext --\u003e Store: Retrieve Context\n                SetCorrelationId --\u003e Store: Update ID\n                GetCorrelationId --\u003e Store: Get/Generate ID\n                Clear --\u003e Store: Delete Context\n            }\n        }\n\n        state fork_state \u003c\u003cfork\u003e\u003e\n\n        Init --\u003e fork_state\n        fork_state --\u003e CopyParentContext: Has Parent Context\n        fork_state --\u003e CreateNewContext: No Parent Context\n\n        CopyParentContext --\u003e Store: Set New AsyncID\n        CreateNewContext --\u003e Store: Generate New ID\n\n        PromiseResolve --\u003e Store: Copy Context to New AsyncID\n        Destroy --\u003e Store: Delete Context for AsyncID\n    }\n\n    state \"Example Flow\" as example {\n        Request --\u003e SetCorrelationId: Upstream ID\n        SetCorrelationId --\u003e ProcessData: Async Operation\n        ProcessData --\u003e GetContext: Get Results\n        GetContext --\u003e Clear: Cleanup\n    }\n\n    AsyncContextId --\u003e BeforeExit: Process Exit\n    BeforeExit --\u003e [*]: Cleanup Hook \u0026 Store\n```\n\n## Flow Diagram Explanation\n\n### Instance Creation\n\n* Program starts by creating a singleton `AsyncContextId` instance\n* Sets up async hooks and initializes the context store (Map)\n\n### Async Hooks Setup\n\n* Hook listens for three main events:\n  * `init`: When new async operations are created\n  * `promiseResolve`: When promises are resolved\n  * `destroy`: When async operations complete\n\n### Context Operations\n\nCustom data is stored in the `meta` attribute. All context / `meta`\noperations use deep cloning to ensure isolation between async operations.\n\n* Main operations on the context store:\n  * `setContext`: Updates context for current async ID\n  * `getContext`: Retrieves context for current async ID\n  * `setCorrelationId`: Sets/updates correlation ID\n  * `getCorrelationId`: Gets or generates correlation ID\n  * `clear`: Removes context for current async ID\n\n### Context Propagation\n\n* When a new async operation starts:\n  * If parent context exists, it's copied to new async ID\n  * If no parent context, new context is created with generated ID\n* When promises resolve, context is copied to new async ID\n* When operations complete, context is cleaned up\n\n### Cleanup\n\n* On process exit:\n  * Disables async hooks\n  * Clears context store\n  * Removes event listeners\n\n### Example Flow\n\n* Shows typical request handling:\n  * Set correlation ID from upstream\n  * Process data asynchronously\n  * Retrieve context for logging\n  * Clear context when done\n\n\n\u003c!--END_SECTION:file:HOWITWORKS.md--\u003e\n\n\u003c!--START_SECTION:jsdoc--\u003e\n## API\n\n### Modules\n\n\u003ctable\u003e\n  \u003cthead\u003e\n    \u003ctr\u003e\n      \u003cth\u003eModule\u003c/th\u003e\u003cth\u003eDescription\u003c/th\u003e\n    \u003c/tr\u003e\n  \u003c/thead\u003e\n  \u003ctbody\u003e\n\u003ctr\u003e\n    \u003ctd\u003e\u003ca href=\"#module_@psenger/async-context-id\"\u003e@psenger/async-context-id\u003c/a\u003e\u003c/td\u003e\n    \u003ctd\u003e\u003cp\u003eA lightweight, powerful correlation ID tracking and context store.\u003c/p\u003e\n\u003c/td\u003e\n    \u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\n### Classes\n\n\u003ctable\u003e\n  \u003cthead\u003e\n    \u003ctr\u003e\n      \u003cth\u003eGlobal\u003c/th\u003e\u003cth\u003eDescription\u003c/th\u003e\n    \u003c/tr\u003e\n  \u003c/thead\u003e\n  \u003ctbody\u003e\n\u003ctr\u003e\n    \u003ctd\u003e\u003ca href=\"#AsyncContextId\"\u003eAsyncContextId\u003c/a\u003e\u003c/td\u003e\n    \u003ctd\u003e\u003cp\u003eA singleton class that tracks correlation IDs across asynchronous operations in Node.js.\nUses async_hooks to automatically propagate correlation context across async boundaries.\u003c/p\u003e\n\u003c/td\u003e\n    \u003c/tr\u003e\n\u003ctr\u003e\n    \u003ctd\u003e\u003ca href=\"#LruMap\"\u003eLruMap\u003c/a\u003e ⇐ \u003ccode\u003eMap\u003c/code\u003e\u003c/td\u003e\n    \u003ctd\u003e\u003cp\u003eA Map extension implementing Least Recently Used (LRU) caching strategy.\nAutomatically removes oldest entries when size limit is reached.\u003c/p\u003e\n\u003c/td\u003e\n    \u003c/tr\u003e\n\u003ctr\u003e\n    \u003ctd\u003e\u003ca href=\"#TimedMap\"\u003eTimedMap\u003c/a\u003e ⇐ \u003ccode\u003eMap\u003c/code\u003e\u003c/td\u003e\n    \u003ctd\u003e\u003cp\u003eA Map extension that automatically deletes entries after a specified time-to-live (TTL).\u003c/p\u003e\n\u003c/td\u003e\n    \u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\n\u003ca name=\"module_@psenger/async-context-id\"\u003e\u003c/a\u003e\n\n### @psenger/async-context-id\nA lightweight, powerful correlation ID tracking and context store.\n\n\u003ca name=\"AsyncContextId\"\u003e\u003c/a\u003e\n\n### AsyncContextId\nA singleton class that tracks correlation IDs across asynchronous operations in Node.js.\nUses async_hooks to automatically propagate correlation context across async boundaries.\n\n**Kind**: global class  \n**Properties**\n\n| Name | Type | Description |\n| --- | --- | --- |\n| contextStore | \u003ccode\u003eMap\u003c/code\u003e | Storage for async context data |\n| hook | \u003ccode\u003eAsyncHook\u003c/code\u003e | The async_hooks instance for tracking async operations |\n| cleanUpFn | \u003ccode\u003efunction\u003c/code\u003e | Cleanup function registered for process exit |\n\n\n* [AsyncContextId](#AsyncContextId)\n    * [new AsyncContextId([options])](#new_AsyncContextId_new)\n    * [.cleanUpFn()](#AsyncContextId+cleanUpFn)\n    * [.getCorrelationId()](#AsyncContextId+getCorrelationId) ⇒ \u003ccode\u003estring\u003c/code\u003e\n    * [.setCorrelationId(correlationId)](#AsyncContextId+setCorrelationId)\n    * [.getContext()](#AsyncContextId+getContext) ⇒ \u003ccode\u003eObject\u003c/code\u003e \\| \u003ccode\u003estring\u003c/code\u003e \\| \u003ccode\u003enumber\u003c/code\u003e \\| \u003ccode\u003eObject\u003c/code\u003e\n    * [.setContext([context])](#AsyncContextId+setContext)\n    * [.clear()](#AsyncContextId+clear)\n\n\u003ca name=\"new_AsyncContextId_new\"\u003e\u003c/a\u003e\n\n#### new AsyncContextId([options])\nThis class provides context propagation across async operations in Node.js\napplications. It maintains correlation IDs and metadata throughout the async execution chain.\n\n**Returns**: [\u003ccode\u003eAsyncContextId\u003c/code\u003e](#AsyncContextId) - The singleton instance  \n\n| Param | Type | Default | Description |\n| --- | --- | --- | --- |\n| [options] | \u003ccode\u003eObject\u003c/code\u003e | \u003ccode\u003e{}\u003c/code\u003e | Configuration options |\n| [options.store] | \u003ccode\u003eMap\u003c/code\u003e | \u003ccode\u003enew Map()\u003c/code\u003e | Optional Map instance for context storage, this package includes both a LRU ( Least Recently Used ) Map and a Timed Map. |\n| [options.correlationIdFn] | \u003ccode\u003efn\u003c/code\u003e |  | Optional function to override default UUID generation. Should return a string |\n\n**Example**  \n```js\n// Using default Map and UUID generation\nconst tracker = new AsyncContextId();\n```\n**Example**  \n```js\n// Using custom LRU Map and correlation ID generator\nconst tracker = new AsyncContextId({\n  store: new LruMap(1000),\n  correlationIdFn: () =\u003e `custom-${Date.now()}`\n});\n```\n\u003ca name=\"AsyncContextId+cleanUpFn\"\u003e\u003c/a\u003e\n\n#### asyncContextId.cleanUpFn()\nremove the listener of itself to prevent memory leaks\n\n**Kind**: instance method of [\u003ccode\u003eAsyncContextId\u003c/code\u003e](#AsyncContextId)  \n\u003ca name=\"AsyncContextId+getCorrelationId\"\u003e\u003c/a\u003e\n\n#### asyncContextId.getCorrelationId() ⇒ \u003ccode\u003estring\u003c/code\u003e\nRetrieves the correlation ID for the current async context.\nCreates a new context with generated ID if none exists.\n\n**Kind**: instance method of [\u003ccode\u003eAsyncContextId\u003c/code\u003e](#AsyncContextId)  \n**Returns**: \u003ccode\u003estring\u003c/code\u003e - The current correlation ID  \n**Throws**:\n\n- \u003ccode\u003eError\u003c/code\u003e If async hooks are not enabled\n\n**Example**  \n```js\nconst correlationId = tracker.getCorrelationId();\nres.setHeader('x-correlation-id', correlationId);\n```\n\u003ca name=\"AsyncContextId+setCorrelationId\"\u003e\u003c/a\u003e\n\n#### asyncContextId.setCorrelationId(correlationId)\nSets the correlation ID for the current async context.\nCreates a new context if none exists.\n\n**Kind**: instance method of [\u003ccode\u003eAsyncContextId\u003c/code\u003e](#AsyncContextId)  \n**Throws**:\n\n- \u003ccode\u003eError\u003c/code\u003e If the correlationId is not a string\n- \u003ccode\u003eError\u003c/code\u003e If async hooks are not enabled\n\n\n| Param | Type | Description |\n| --- | --- | --- |\n| correlationId | \u003ccode\u003estring\u003c/code\u003e | The correlation ID to set |\n\n**Example**  \n```js\nconst upstreamId = req.headers['x-correlation-id'];\nif (upstreamId) {\n  tracker.setCorrelationId(upstreamId);\n}\n```\n\u003ca name=\"AsyncContextId+getContext\"\u003e\u003c/a\u003e\n\n#### asyncContextId.getContext() ⇒ \u003ccode\u003eObject\u003c/code\u003e \\| \u003ccode\u003estring\u003c/code\u003e \\| \u003ccode\u003enumber\u003c/code\u003e \\| \u003ccode\u003eObject\u003c/code\u003e\nRetrieves the complete context object for the current async operation.\nCreates a new context if none exists.\n\n**Kind**: instance method of [\u003ccode\u003eAsyncContextId\u003c/code\u003e](#AsyncContextId)  \n**Returns**: \u003ccode\u003eObject\u003c/code\u003e - The correlation context\u003ccode\u003estring\u003c/code\u003e - context.correlationId - The correlation ID\u003ccode\u003enumber\u003c/code\u003e - context.startTime - Unix timestamp of context creation\u003ccode\u003eObject\u003c/code\u003e - context.metadata - Custom metadata object  \n**Throws**:\n\n- \u003ccode\u003eError\u003c/code\u003e If async hooks are not enabled\n\n**Example**  \n```js\nconst context = tracker.getContext();\nconsole.log({\n  correlationId: context.correlationId,\n  duration: Date.now() - context.startTime,\n  metadata: context.metadata\n});\n```\n\u003ca name=\"AsyncContextId+setContext\"\u003e\u003c/a\u003e\n\n#### asyncContextId.setContext([context])\nUpdates the context for the current async operation.\nCreates a new context if none exists. Preserves existing correlationId\nand startTime unless explicitly overridden.\n\n**Kind**: instance method of [\u003ccode\u003eAsyncContextId\u003c/code\u003e](#AsyncContextId)  \n**Throws**:\n\n- \u003ccode\u003eError\u003c/code\u003e If async hooks are not enabled\n- \u003ccode\u003eError\u003c/code\u003e If context is not an object\n\n\n| Param | Type | Default | Description |\n| --- | --- | --- | --- |\n| [context] | \u003ccode\u003eObject\u003c/code\u003e | \u003ccode\u003e{}\u003c/code\u003e | The context object to merge |\n| [context.correlationId] | \u003ccode\u003estring\u003c/code\u003e |  | Optional correlation ID override |\n| [context.metadata] | \u003ccode\u003eObject\u003c/code\u003e |  | Optional metadata to merge |\n\n**Example**  \n```js\n// Add request context\ntracker.setContext({\n  metadata: {\n    operation: 'processData',\n    requestId: req.id,\n    userId: req.user.id\n  }\n});\n```\n\u003ca name=\"AsyncContextId+clear\"\u003e\u003c/a\u003e\n\n#### asyncContextId.clear()\nRemoves the correlation context for the current async operation.\n\n**Kind**: instance method of [\u003ccode\u003eAsyncContextId\u003c/code\u003e](#AsyncContextId)  \n**Throws**:\n\n- \u003ccode\u003eError\u003c/code\u003e If async hooks are not enabled\n\n**Example**  \n```js\ntry {\n  await processRequest(data);\n} finally {\n  tracker.clear();\n}\n```\n\u003ca name=\"LruMap\"\u003e\u003c/a\u003e\n\n### LruMap ⇐ \u003ccode\u003eMap\u003c/code\u003e\nA Map extension implementing Least Recently Used (LRU) caching strategy.\nAutomatically removes oldest entries when size limit is reached.\n\n**Kind**: global class  \n**Extends**: \u003ccode\u003eMap\u003c/code\u003e  \n\n* [LruMap](#LruMap) ⇐ \u003ccode\u003eMap\u003c/code\u003e\n    * [new LruMap(maxSize)](#new_LruMap_new)\n    * [.set(key, value)](#LruMap+set) ⇒ \u003ccode\u003ethis\u003c/code\u003e\n\n\u003ca name=\"new_LruMap_new\"\u003e\u003c/a\u003e\n\n#### new LruMap(maxSize)\nCreates an LRU cache with specified maximum size.\n\n\n| Param | Type | Description |\n| --- | --- | --- |\n| maxSize | \u003ccode\u003enumber\u003c/code\u003e | Maximum number of entries |\n\n**Example**  \n```js\nconst cache = new LruMap(3);\ncache.set('a', 1).set('b', 2).set('c', 3);\ncache.set('d', 4); // Removes 'a', now contains b,c,d\n```\n\u003ca name=\"LruMap+set\"\u003e\u003c/a\u003e\n\n#### lruMap.set(key, value) ⇒ \u003ccode\u003ethis\u003c/code\u003e\nSets a value, removing oldest entry if size limit reached.\n\n**Kind**: instance method of [\u003ccode\u003eLruMap\u003c/code\u003e](#LruMap)  \n**Returns**: \u003ccode\u003ethis\u003c/code\u003e - The LruMap instance for chaining  \n\n| Param | Type | Description |\n| --- | --- | --- |\n| key | \u003ccode\u003e\\*\u003c/code\u003e | The key to set |\n| value | \u003ccode\u003e\\*\u003c/code\u003e | The value to store |\n\n**Example**  \n```js\nconst cache = new LruMap(2);\ncache.set('key1', 'value1')\n     .set('key2', 'value2')\n     .set('key3', 'value3'); // Removes key1\n```\n\u003ca name=\"TimedMap\"\u003e\u003c/a\u003e\n\n### TimedMap ⇐ \u003ccode\u003eMap\u003c/code\u003e\nA Map extension that automatically deletes entries after a specified time-to-live (TTL).\n\n**Kind**: global class  \n**Extends**: \u003ccode\u003eMap\u003c/code\u003e  \n\n* [TimedMap](#TimedMap) ⇐ \u003ccode\u003eMap\u003c/code\u003e\n    * [new TimedMap(ttl)](#new_TimedMap_new)\n    * [.set(key, value)](#TimedMap+set) ⇒ \u003ccode\u003ethis\u003c/code\u003e\n    * [.delete(key)](#TimedMap+delete) ⇒ \u003ccode\u003eboolean\u003c/code\u003e\n\n\u003ca name=\"new_TimedMap_new\"\u003e\u003c/a\u003e\n\n#### new TimedMap(ttl)\nCreates a new TimedMap instance.\n\n\n| Param | Type | Description |\n| --- | --- | --- |\n| ttl | \u003ccode\u003enumber\u003c/code\u003e | Time-to-live in milliseconds for each key-value pair |\n\n**Example**  \n```js\nconst cache = new TimedMap(5000); // 5 second TTL\ncache.set('key1', 'value1');\nconsole.log(cache.get('key1')); // 'value1'\n// After 5 seconds:\nconsole.log(cache.get('key1')); // undefined\n```\n**Example**  \n```js\n// Resetting TTL on value update\nconst cache = new TimedMap(2000);\ncache.set('user', { name: 'Alice' });\n// 1 second later:\ncache.set('user', { name: 'Alice', age: 30 }); // Resets the 2-second timer\n```\n\u003ca name=\"TimedMap+set\"\u003e\u003c/a\u003e\n\n#### timedMap.set(key, value) ⇒ \u003ccode\u003ethis\u003c/code\u003e\nSets a value in the map with an automatic deletion timer.\nIf the key already exists, its timer is reset.\n\n**Kind**: instance method of [\u003ccode\u003eTimedMap\u003c/code\u003e](#TimedMap)  \n**Returns**: \u003ccode\u003ethis\u003c/code\u003e - The TimedMap instance for chaining  \n\n| Param | Type | Description |\n| --- | --- | --- |\n| key | \u003ccode\u003e\\*\u003c/code\u003e | The key to set |\n| value | \u003ccode\u003e\\*\u003c/code\u003e | The value to store |\n\n**Example**  \n```js\nconst cache = new TimedMap(10000);\ncache.set('apiKey', 'xyz123')\n     .set('timestamp', Date.now());\n```\n\u003ca name=\"TimedMap+delete\"\u003e\u003c/a\u003e\n\n#### timedMap.delete(key) ⇒ \u003ccode\u003eboolean\u003c/code\u003e\nDeletes a key-value pair and its associated timer.\n\n**Kind**: instance method of [\u003ccode\u003eTimedMap\u003c/code\u003e](#TimedMap)  \n**Returns**: \u003ccode\u003eboolean\u003c/code\u003e - True if the element was deleted, false if it didn't exist  \n\n| Param | Type | Description |\n| --- | --- | --- |\n| key | \u003ccode\u003e\\*\u003c/code\u003e | The key to delete |\n\n**Example**  \n```js\nconst cache = new TimedMap(5000);\ncache.set('temp', 'data');\ncache.delete('temp'); // Manually delete before TTL expires\n```\n\n\u003c!--END_SECTION:jsdoc--\u003e\n\n\u003c!--START_SECTION:file:USAGE.md--\u003e\n## Usage\n\n### Enhanced Logging with Console Monkey Patching\n\nThis example demonstrates how to automatically inject correlation IDs into console logs\nusing [monkey patching](https://en.wikipedia.org/wiki/Monkey_patch). The code below\nintercepts standard console methods and prepends each log message with its log level\nand correlation ID.\n\neg `LOG-LEVEL CORRELATION-ID`\n\n\u003c!--START_CODE_FENCE_SECTION:javascript:file:examples/monkey-patch-logs.js--\u003e\n```javascript\nconst fs = require('fs')\nconst path = require('path')\nconst util = require('util')\nconst {AsyncContextId} = require('../dist/index')\nconst TRACKER = new AsyncContextId()\nconst logFile = path.join(__dirname, 'app.log')\nconst original = {\n  log: console.log,\n  error: console.error,\n  warn: console.warn,\n  debug: console.debug,\n  info: console.info\n}\ntry {\n  if (fs.existsSync(logFile)) {\n    fs.writeFileSync(logFile, '')\n  }\n} catch (err) {\n  console.error('Error handling log file:', err)\n}\nfunction formatApacheErrorTimestamp(date = new Date()) {\n  const days = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']\n  const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']\n  return `[${days[date.getDay()]} ${months[date.getMonth()]} ${String(date.getDate()).padStart(2, '0')} ` +\n    `${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}:` +\n    `${String(date.getSeconds()).padStart(2, '0')}.${String(date.getMilliseconds()).padStart(3, '0')} ` +\n    `${date.getFullYear()}]`\n}\nObject.keys(original).forEach(method =\u003e {\n  console[method] = function (...args) {\n    const prefix = `${formatApacheErrorTimestamp()} [${method.toLowerCase()}] [${TRACKER.getContext().correlationId}] ${TRACKER.getContext()?.metadata?.fullName} `\n    if (typeof args[0] === 'string') {\n      args[0] = prefix + args[0]\n    } else {\n      args.unshift(prefix)\n    }\n    original[method].apply(console, args)\n    fs.appendFileSync(logFile, util.format(...args) + '\\n')\n  }\n})\n\n```\n\u003c!--END_CODE_FENCE_SECTION:javascript:file:examples/monkey-patch-logs.js--\u003e\n\n### Express Middleware\n\nThis Express middleware automatically manages correlation IDs across HTTP requests. It\nextracts the correlation ID from incoming request headers, propagates it through the\nrequest lifecycle, and includes it in the response headers.\n\n**Note:** Register this middleware early in your Express application's middleware chain\nto ensure correlation IDs are available throughout the entire request lifecycle.\n\n#### `correlation-middleware.js`\n\n\u003c!--START_CODE_FENCE_SECTION:javascript:file:examples/correlation-middleware.js--\u003e\n```javascript\nconst {AsyncContextId} = require('../dist/index') // '@psenger/async-context-id'\nconst asyncContextId = new AsyncContextId()\nconst correlationMiddleware = () =\u003e (req, res, next) =\u003e {\n  try {\n    asyncContextId.clear()\n    let correlationId = req.headers['x-correlation-id']\n    correlationId = asyncContextId.setCorrelationId(correlationId)\n    res.setHeader('x-correlation-id', correlationId)\n    res.on('finish', () =\u003e {\n      asyncContextId.clear()\n    })\n    next()\n  } catch (error) {\n    next(error)\n  }\n}\nmodule.exports = correlationMiddleware\n\n```\n\u003c!--END_CODE_FENCE_SECTION:javascript:file:examples/correlation-middleware.js--\u003e\n\n#### `app.js`\n\n\u003c!--START_CODE_FENCE_SECTION:javascript:file:examples/app.js--\u003e\n```javascript\nconst express = require('express')\nconst app = express()\nconst router = express.Router()\nconst {systemTimer} = require('./timer')\nconst controller = require('./controller')\nconst correlationMiddleware = require('./correlation-middleware')\nrequire('./monkey-patch-logs')\nsystemTimer()\napp.use(express.json())\napp.use(express.urlencoded({extended: false}))\napp.use(correlationMiddleware())\nrouter.post('/:id', controller)\napp.use('/', router)\nmodule.exports = app\n\n```\n\u003c!--END_CODE_FENCE_SECTION:javascript:file:examples/app.js--\u003e\n\n#### `simple-controller.js`\n\n\u003c!--START_CODE_FENCE_SECTION:javascript:file:examples/simple-controller.js--\u003e\n```javascript\nconst {AsyncContextId} = require('../dist/index') // '@psenger/async-context-id'\nconst TRACKER = new AsyncContextId()\nmodule.exports = function (req, res) {\n  const id = req.params.id\n  const correlationId = TRACKER.getContext().correlationId\n  console.log(`${correlationId} saw ${id} in controller`)\n  TRACKER.setContext({\n    metadata: {\n      id,\n    }\n  })\n  // do something else here which will expose meta upstream\n}\n\n```\n\u003c!--END_CODE_FENCE_SECTION:javascript:file:examples/simple-controller.js--\u003e\n\n### Winston Logger Integration\n\n\u003c!--START_CODE_FENCE_SECTION:javascript:file:examples/winston-logger.js--\u003e\n```javascript\nconst winston = require('winston')\nconst {AsyncContextId} = require('../dist/index') // '@psenger/async-context-id'\n\nconst TRACKER = new AsyncContextId()\n\nconst logger = winston.createLogger({\n  format: winston.format.combine(\n    winston.format.timestamp(),\n    winston.format.json(),\n    winston.format((info) =\u003e {\n      const context = TRACKER.getContext();\n      return {\n        ...info,\n        correlationId: context?.correlationId || 'no-correlation-id',\n        metadata: context?.metadata || {}\n      };\n    })()\n  ),\n  transports: [\n    new winston.transports.Console()\n  ]\n});\n\n```\n\u003c!--END_CODE_FENCE_SECTION:javascript:file:examples/winston-logger.js--\u003e\n\n## ‼️ Caution - Memory Management Strategies\n\nAll scaling systems degrade when affected by memory leaks. This module hosts a single map\nand addresses such leaks through two configurable Map implementations for context tracking:\n\n* LRU (Least Recently Used) Map\n* Timed Map\n\nThese maps can be configured during AsyncContextId initialization. As AsyncContextId operates\nas a singleton, the map implementation must be set at creation and remains immutable.\n\n## ‼️ Caution - Correlation ID with `UUID`\n\nThe default implementation of correlation ID uses non-cryptographic UUID generation to prevent recursive\nasync hook triggers that would otherwise occur with Node's Crypto module (internal crypto operations\nwould initiate new async hooks). Since this is based on UUID v4, it may be necessary for consumers\nto increase the complexity of the correlation ID. Therefore, this functionality has been exposed as\nan option ( `correlationIdFn` ).\n\ne.g.\n```javascript\n// Safe UUID v4 implementation without Crypto\ngenerateCorrelationId() {\n  if (this.correlationIdFn) {\n    return this.correlationIdFn()\n  }\n  return \"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx\".replace(/[xy]/g, (c) =\u003e {\n    const r = (Math.random() * 16) | 0;\n    const v = c === \"x\" ? r : (r \u0026 0x3) | 0x8;\n    return v.toString(16);\n  });\n}\n```\n\n## ‼️ Limitations\n\nThe async hooks implementation tracks context through EventEmitters, Timers, and Node.js\ncore callbacks. However, as async hooks remain experimental, context propagation should be\ntested extensively in production like scenarios. In the event that something does not work as\nexpected, there are some untested patterns and suggested work around. Remember it is better\nto error on the side of caution rather than create a memory leak.\n\n### Untested pattern - **Process Next Tick**\n\n```javascript\nprocess.nextTick(() =\u003e {\n    // context may get lost\n    const context = tracker.getContext(); // will create new context\n});\n```\n\n### Untested pattern - **Node.js Core Module Callbacks** (like fs, http, etc.):\n\n```javascript\nconst fs = require('fs');\nfs.readFile('somefile.txt', (err, data) =\u003e {\n    // context may get lost\n    const context = tracker.getContext(); // will create new context\n});\n```\n\n### Solution to Untested pattern\n\nDespite the untested patterns, these callbacks can be wrapped and bound. Word of caution, Arrow Functions\ncan not be bound therefore, you must use `function` declaration.\n\n```javascript\nclass CorrelationTracker {\n    bindCallback(fn) {\n        const currentContext = this.getContext();\n        return (...args) =\u003e {\n            const asyncId = asyncHooks.executionAsyncId();\n            this.correlationStore.set(asyncId, JSON.parse(JSON.stringify(currentContext)));\n            try {\n                return fn(...args);\n            } finally {\n                this.correlationStore.delete(asyncId);\n            }\n        };\n    }\n}\n\n// Usage:\nemitter.on('someEvent', tracker.bindCallback(() =\u003e {\n    // context is preserved\n    const context = tracker.getContext();\n}));\n```\n\n2. Or use the async_hooks executionAsyncResource when available:\n\n```javascript\nconst asyncHooks = require('async_hooks');\nconst { AsyncResource } = require('async_hooks');\n\nclass TrackedEmitter extends EventEmitter {\n    emit(event, ...args) {\n        const asyncResource = new AsyncResource(event);\n        return asyncResource.runInAsyncScope(() =\u003e {\n            return super.emit(event, ...args);\n        });\n    }\n}\n```\n\n\u003c!--END_SECTION:file:USAGE.md--\u003e\n\n\u003c!--START_SECTION:file:CONTRIBUTING.md--\u003e\n\n## Contributing\n\nThanks for contributing! 😁 Here are some rules that will make your change to\nmarkdown-fences fruitful.\n\n### Rules\n\n* Raise a ticket to the feature or bug can be discussed\n* Pull requests are welcome, but must be accompanied by a ticket approved by the repo owner\n* You are expected to add a unit test or two to cover the proposed changes.\n* Please run the tests and make sure tests are all passing before submitting your pull request\n* Do as the Romans do and stick with existing whitespace and formatting conventions (i.e., tabs instead of spaces, etc)\n  * we have provided the following: `.editorconfig` and `.eslintrc`\n  * Don't tamper with or change `.editorconfig` and `.eslintrc`\n* Please consider adding an example under examples/ that demonstrates any new functionality\n\n### Commit Message\n\nThis module uses [release-please](https://github.com/googleapis/release-please) which\nneeds commit messages to look like the following [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/)\n\n```\n\u003ctype\u003e[optional scope]: \u003cdescription\u003e\n\n[optional body]\n\n```\n\n**type** is typically `fix`, `feat`. When **type** ends with a `!` or is `BREAKING CHANGE` it indicates this is a breaking change.\n\n**type** should be followed by a short description, **\u003cdescription\u003e**\n\n**optional body** can have more detail\n\n### Testing\n\n* All tests are expected to work\n* Tests are based off of `dist/index.js` **NOT** your src code. Therefore, you should BUILD it first.\n* Coverage should not go down, and I acknowledge it is very difficult to get the tests to 100%\n\n\u003c!--END_SECTION:file:CONTRIBUTING.md--\u003e\n\n## License\n\n\u003c!--START_SECTION:file:LICENSE--\u003e\nMIT License\n\nCopyright (c) 2025 Philip A Senger\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n\n\u003c!--END_SECTION:file:LICENSE--\u003e\n\n\u003c!--START_SECTION:file:THIRD_PARTY_NOTICES.md--\u003e\n\n## Acknowledgments\n\nThis project directly uses the following open-source packages:\n\n### Dependencies\n\n- None\n\n### Development Dependencies\n\n- [@psenger/markdown-fences](https://github.com/psenger/markdown-fences) - MIT License\n- [eslint-config-prettier](https://github.com/prettier/eslint-config-prettier) - MIT License\n- [eslint-plugin-jest](https://github.com/jest-community/eslint-plugin-jest) - MIT License\n- [eslint-plugin-prettier](https://github.com/prettier/eslint-plugin-prettier) - MIT License\n- [eslint](https://github.com/eslint/eslint) - MIT License\n- [jest-html-reporters](https://github.com/Hazyzh/jest-html-reporters) - MIT License\n- [jest](https://github.com/jestjs/jest) - MIT License\n- [jsdoc](https://github.com/jsdoc/jsdoc) - Apache-2.0 License\n- [license-checker](https://github.com/davglass/license-checker) - BSD-3-Clause License\n- [markdown-toc](https://github.com/jonschlinkert/markdown-toc) - MIT License\n- [prettier](https://github.com/prettier/prettier) - MIT License\n- [rimraf](https://github.com/isaacs/rimraf) - ISC License\n- [rollup](https://github.com/rollup/rollup) - MIT License\n- [standard-version](https://github.com/conventional-changelog/standard-version) - ISC License\n\n\u003c!--END_SECTION:file:THIRD_PARTY_NOTICES.md--\u003e\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpsenger%2Fasync-context-id","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fpsenger%2Fasync-context-id","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpsenger%2Fasync-context-id/lists"}