@marceloraineri/async-context 1.0.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1 +1,85 @@
1
- # SyncContext
1
+ # AsyncContext
2
+
3
+ > Request-scoped context storage backed by Node.js `AsyncLocalStorage`, with first-class Express integration.
4
+
5
+ AsyncContext is a tiny utility library that standardizes how you propagate contextual data across asynchronous flows. It offers a singleton `Context` wrapper around `AsyncLocalStorage` plus helpers to enrich the active store and an Express middleware that bootstraps a fresh context for every incoming request.
6
+
7
+ ## Why AsyncContext?
8
+
9
+ - **Consistent async context** – Create one logical context per request, job, or background task without passing parameters through every function call.
10
+ - **Drop-in API** – Call `Context.addValue` or `Context.addObjectValue` anywhere inside the active flow to append metadata.
11
+ - **Observability ready** – Ship correlation IDs, tenant information, user data, or tracing metadata through your stack.
12
+ - **Framework friendly** – Includes an `AsyncContextExpresssMiddleware` that assigns a unique `instance_id` to each Express request and runs all downstream handlers inside that context.
13
+
14
+ ## Installation
15
+
16
+ ```bash
17
+ npm i @marceloraineri/a@marceloraineri/async-context
18
+ ```
19
+
20
+ When developing locally inside this repo, import from the relative `core` entry point instead.
21
+
22
+ ## Quick start
23
+
24
+ ```ts
25
+ import { Context } from "@marceloraineri/async-context";
26
+ Context.addValue("user", { id: 42, name: "Ada" });
27
+
28
+ await Promise.resolve();
29
+
30
+ const store = Context.getInstance().getStore();
31
+ console.log(store.requestId); // 184fa9a3-f967-4a98-9d8f-57152e7cbe64
32
+ console.log(store.user); // { id: 42, name: "Ada" }
33
+ ```
34
+
35
+ ## Express middleware
36
+
37
+ `AsyncContextExpresssMiddleware` (note the triple “s”) creates a new context for every Express request, seeds it with a UUID `instance_id`, and ensures the context is available throughout the request lifecycle.
38
+
39
+ ```ts
40
+ import express from "express";
41
+ import {
42
+ AsyncContextExpresssMiddleware,
43
+ Context,
44
+ } from "@marceloraineri/async-context";
45
+
46
+ const app = express();
47
+
48
+ app.use(AsyncContextExpresssMiddleware);
49
+
50
+ app.get("/ping", (_req, res) => {
51
+ const store = Context.getInstance().getStore();
52
+ res.json({ instanceId: store?.instance_id ?? null });
53
+ });
54
+
55
+ app.listen(3000, () => console.log("API listening on :3000"));
56
+ ```
57
+
58
+ ## API reference
59
+
60
+ ### `Context.getInstance(): AsyncLocalStorage`
61
+ Returns (and lazily instantiates) the singleton `AsyncLocalStorage` used by the library. You typically call `run(store, callback)` on this instance to spawn a new context.
62
+
63
+ ### `Context.addObjectValue(values: Record<string, unknown>): Record<string, unknown>`
64
+ Merges the provided object into the active context. Also throws if no context is active.
65
+
66
+ ### `AAsyncContextExpresssMiddleware(req, res, next)`
67
+ Express middleware that:
68
+
69
+ 1. Generates a UUID via `crypto.randomUUID()`.
70
+ 2. Calls `Context.getInstance().run({ instance_id: uuid }, () => next())`.
71
+ 3. Makes the context (and `instance_id`) available to any downstream code.
72
+
73
+ ## Best practices & caveats
74
+
75
+ - Avoid replacing the entire store object manually; instead mutate it through `addValue`/`addObjectValue` to keep shared references intact.
76
+ - `AsyncLocalStorage` state is scoped to a single Node.js process. If you spawn workers or separate processes, each will maintain its own context.
77
+ - Be mindful of long-lived contexts: if you never exit a `run` callback (e.g., forgetting to call `next()` in Express), the store will never be released.
78
+
79
+ ## Contributing
80
+
81
+ Issues and pull requests are welcome. Please include reproduction steps or tests whenever you propose a change to the async context behavior.
82
+
83
+ ## License
84
+
85
+ Specify the desired license (e.g., MIT) here.
@@ -0,0 +1,76 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
2
+
3
+ /**
4
+ * Provides an application-wide asynchronous context using Node.js AsyncLocalStorage.
5
+ * Allows storing and retrieving key/value data within the active async execution flow.
6
+ */
7
+ export class Context {
8
+ /**
9
+ * Singleton instance of AsyncLocalStorage.
10
+ * @type {AsyncLocalStorage<unknown>}
11
+ */
12
+ public static asyncLocalStorageInstance: AsyncLocalStorage<unknown>;
13
+
14
+ /**
15
+ * Private constructor initializes the AsyncLocalStorage instance.
16
+ * Called automatically when the instance does not yet exist.
17
+ * @private
18
+ */
19
+ private constructor() {
20
+ Context.asyncLocalStorageInstance = new AsyncLocalStorage();
21
+ }
22
+
23
+ /**
24
+ * Returns the global AsyncLocalStorage instance, creating it if necessary.
25
+ *
26
+ * @returns {AsyncLocalStorage<unknown>} The AsyncLocalStorage singleton.
27
+ */
28
+ static getInstance(): AsyncLocalStorage<unknown> {
29
+ if (!Context.asyncLocalStorageInstance) {
30
+ new Context();
31
+ }
32
+ return Context.asyncLocalStorageInstance;
33
+ }
34
+
35
+ /**
36
+ * Adds a single key/value pair to the active asynchronous context.
37
+ *
38
+ * @param {string} key - Key to store inside the context.
39
+ * @param {*} value - Value to associate with the given key.
40
+ * @returns {Record<string, any>} The updated context object.
41
+ *
42
+ * @throws {Error} If called outside of an active `Context.getInstance().run()`.
43
+ */
44
+ static addValue(key: string, value: any) {
45
+ const contextObject = Context.getInstance().getStore() as Record<
46
+ string,
47
+ any
48
+ >;
49
+ if (!contextObject)
50
+ throw new Error(
51
+ "No active context found. Use Context.getInstance().run()."
52
+ );
53
+
54
+ contextObject[key] = value;
55
+ return contextObject;
56
+ }
57
+
58
+ /**
59
+ * Merges an object of values into the active asynchronous context.
60
+ *
61
+ * @param {Record<string, any>} object - Object containing key/value pairs to merge.
62
+ * @returns {Record<string, any>} The merged context object.
63
+ *
64
+ * @throws {Error} If called outside of an active `Context.getInstance().run()`.
65
+ */
66
+ static addObjectValue(object: Record<string, any>) {
67
+ const contextObject = Context.getInstance().getStore();
68
+ if (!contextObject)
69
+ throw new Error(
70
+ "No active context found. Use Context.getInstance().run()."
71
+ );
72
+
73
+ const merged = Object.assign(contextObject, object);
74
+ return merged;
75
+ }
76
+ }
package/core/index.ts ADDED
@@ -0,0 +1,2 @@
1
+ export { Context } from './context';
2
+ export { AsyncContextExpresssMiddleware } from './integrations/express';
@@ -0,0 +1,56 @@
1
+ import type * as http from "node:http";
2
+ import crypto from "node:crypto";
3
+ import { Context } from "../..";
4
+
5
+ /**
6
+ * Express middleware that initializes a new asynchronous context
7
+ * for each incoming request.
8
+ *
9
+ * This middleware assigns a unique `instance_id` (UUID) to the
10
+ * request lifecycle and stores it inside AsyncLocalStorage,
11
+ * allowing the application to later retrieve per-request data
12
+ * without passing parameters through function calls.
13
+ *
14
+ * It is designed to simplify building request-scoped state,
15
+ * such as logging correlation IDs or storing metadata across
16
+ * asynchronous operations.
17
+ *
18
+ * @function AsyncContextExpressMiddleware
19
+ *
20
+ * @param {http.IncomingMessage} req - The current HTTP request.
21
+ * @param {http.ServerResponse} res - The current HTTP response.
22
+ * @param {Function} next - Express continuation callback.
23
+ *
24
+ * @example
25
+ * // Usage in an Express application
26
+ * import express from "express";
27
+ * import { AsyncContextExpressMiddleware } from "@marceloraineri/async-context";
28
+ *
29
+ * const app = express();
30
+ *
31
+ * app.use(AsyncContextExpressMiddleware);
32
+ *
33
+ * app.get("/test", (req, res) => {
34
+ * const context = Context.getInstance().getStore();
35
+ * console.log(context.instance_id); // Unique per request
36
+ * res.send("OK");
37
+ * });
38
+ *
39
+ * @description
40
+ * A new asynchronous context is created via:
41
+ * `Context.getInstance().run({ instance_id: <uuid> }, ...)`
42
+ *
43
+ * This ensures that all async operations inside the request
44
+ * share the same context object until the request completes.
45
+ */
46
+
47
+ export function AsyncContextExpresssMiddleware(
48
+ req: http.IncomingMessage,
49
+ res: http.ServerResponse,
50
+ next: () => void
51
+ ) {
52
+ const uuid = crypto.randomUUID();
53
+ const LocalStorageInstance = Context.getInstance();
54
+
55
+ LocalStorageInstance.run({ instance_id: uuid }, () => next());
56
+ }
package/package.json CHANGED
@@ -1,10 +1,12 @@
1
1
  {
2
2
  "name": "@marceloraineri/async-context",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "",
5
- "main": "index.js",
5
+ "main": "core/index.ts",
6
6
  "scripts": {
7
- "test": "echo \"Error: no test specified\" && exit 1"
7
+ "test": "echo \"Error: no test specified\" && exit 1",
8
+ "dev": "tsx watch --inspect core/index.ts",
9
+ "build": "tsc"
8
10
  },
9
11
  "repository": {
10
12
  "type": "git",
@@ -18,6 +20,10 @@
18
20
  "homepage": "https://github.com/ohraineri/AsyncContext#readme",
19
21
  "devDependencies": {
20
22
  "@types/node": "^24.10.1",
23
+ "eslint": "^9.39.1",
24
+ "ts-node": "^10.9.2",
25
+ "ts-node-dev": "^2.0.0",
26
+ "tsx": "^4.20.6",
21
27
  "typescript": "^5.9.3"
22
28
  }
23
29
  }
package/index.js DELETED
@@ -1,31 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.Context = void 0;
4
- var node_async_hooks_1 = require("node:async_hooks");
5
- var Context = /** @class */ (function () {
6
- function Context() {
7
- Context.asyncLocalStorageInstance = new node_async_hooks_1.AsyncLocalStorage();
8
- }
9
- Context.getInstance = function () {
10
- if (!Context.asyncLocalStorageInstance) {
11
- new Context();
12
- }
13
- return Context.asyncLocalStorageInstance;
14
- };
15
- Context.addValue = function (key, value) {
16
- var contextObject = Context.getInstance().getStore();
17
- if (!contextObject)
18
- throw new Error('Nenhum contexto ativo encontrado. Use Context.getInstance().run().');
19
- contextObject[key] = value;
20
- return contextObject;
21
- };
22
- Context.addObjectValue = function (object) {
23
- var contextObject = Context.getInstance().getStore();
24
- if (!contextObject)
25
- throw new Error('Nenhum contexto ativo encontrado. Use Context.getInstance().run().');
26
- var merged = Object.assign(contextObject, object);
27
- return merged;
28
- };
29
- return Context;
30
- }());
31
- exports.Context = Context;
package/index.ts DELETED
@@ -1,38 +0,0 @@
1
- import { AsyncLocalStorage } from 'node:async_hooks';
2
-
3
- type objectStorageType = {
4
- [key: string] : unknown
5
- }
6
-
7
- export class Context {
8
- static asyncLocalStorageInstance: AsyncLocalStorage<unknown>;
9
-
10
- private constructor() {
11
- Context.asyncLocalStorageInstance = new AsyncLocalStorage();
12
- }
13
-
14
- static getInstance() {
15
- if (!Context.asyncLocalStorageInstance) {
16
- new Context();
17
- }
18
- return Context.asyncLocalStorageInstance;
19
- }
20
-
21
- static addValue(key: string, value: any) {
22
- const contextObject = Context.getInstance().getStore() as Record<string, any>;
23
- if (!contextObject)
24
- throw new Error('Nenhum contexto ativo encontrado. Use Context.getInstance().run().');
25
-
26
- contextObject[key] = value;
27
- return contextObject;
28
- }
29
-
30
- static addObjectValue(object: Record<string, any>) {
31
- const contextObject = Context.getInstance().getStore();
32
- if (!contextObject)
33
- throw new Error('Nenhum contexto ativo encontrado. Use Context.getInstance().run().');
34
-
35
- const merged = Object.assign(contextObject, object);
36
- return merged;
37
- }
38
- }