@push.rocks/smartcontext
A module to enrich logs with context, featuring async log contexts and scope management.
Install
Make sure you have Node.js and npm installed, then run:
npm install @push.rocks/smartcontext
This will install the library and its dependencies into your local node_modules
folder.
Usage
The @push.rocks/smartcontext
module provides an efficient way to enrich your logging (or any functionality) with contextual information. It uses asynchronous context management to support hierarchical scopes—particularly handy when dealing with complex or nested asynchronous operations in Node.js.
Setting Up and Basic Usage
First, import the necessary classes:
import { AsyncContext } from '@push.rocks/smartcontext';
Creating a Context
An AsyncContext
manages a top-level AsyncStore
. You can attach any data you want to that store. For example:
const asyncContext = new AsyncContext();
// Add data to the top-level store
asyncContext.store.add('username', 'john_doe');
console.log(asyncContext.store.get('username')); // 'john_doe'
Using runScoped
The runScoped
method now creates a child store automatically and makes that child store accessible via asyncContext.store
only within the passed-in function. Once that scoped function completes, asyncContext.store
reverts to the original (parent) store.
Example
await asyncContext.runScoped(async () => {
// While in this scope, asyncContext.store refers to the child store.
asyncContext.store.add('transactionId', 'txn_123456');
console.log(asyncContext.store.get('transactionId')); // 'txn_123456'
// Data from the parent store is also accessible in the child
console.log(asyncContext.store.get('username')); // 'john_doe'
});
// Outside runScoped, asyncContext.store is the parent store again
console.log(asyncContext.store.get('transactionId')); // undefined
- Child Store Access: Within the scope,
asyncContext.store
points to a child store. - Parent Data: Any keys from the parent are still visible in the child (unless overridden).
- No Cross-Contamination: Keys added in the child store don’t persist in the parent after the scope ends.
Isolating and Deleting Data
If you add a key within a runScoped
block, it stays there unless you intentionally move it to the parent. Also, if you delete a key in the child, it won’t remove it from the parent store:
// Parent store has some initial data
asyncContext.store.add('deletableKey', 'initialValue');
await asyncContext.runScoped(async () => {
console.log(asyncContext.store.get('deletableKey')); // 'initialValue'
asyncContext.store.delete('deletableKey'); // Delete in child
console.log(asyncContext.store.get('deletableKey')); // undefined (child only)
});
// Outside scope, parent's data is intact
console.log(asyncContext.store.get('deletableKey')); // 'initialValue'
Multiple Scopes
You can create multiple scopes, each maintaining its own data isolation. That way, concurrent or sequential operations don’t overwrite or “contaminate” each other’s data in the shared context.
// Scope 1
await asyncContext.runScoped(async () => {
asyncContext.store.add('key1', 'value1');
console.log(asyncContext.store.get('key1')); // 'value1'
});
// Scope 2
await asyncContext.runScoped(async () => {
asyncContext.store.add('key2', 'value2');
console.log(asyncContext.store.get('key2')); // 'value2'
});
// After both scopes finish, neither key1 nor key2 is visible to the parent
console.log(asyncContext.store.get('key1')); // undefined
console.log(asyncContext.store.get('key2')); // undefined
Deeper Nesting
If you nest runScoped
calls, each subsequent call creates another child store, which inherits from the store that’s currently active. This allows you to build deeper context layers when needed.
await asyncContext.runScoped(async () => {
asyncContext.store.add('layer1', true);
await asyncContext.runScoped(async () => {
asyncContext.store.add('layer2', true);
// This store sees data from both layer1 and layer2
console.log(asyncContext.store.get('layer1')); // true
console.log(asyncContext.store.get('layer2')); // true
});
// Only layer1 data remains in scope here; layer2 is gone
console.log(asyncContext.store.get('layer2')); // undefined
});
Testing
Below is a simplified test example using tapbundle. It demonstrates runScoped()
creating isolated child stores and verifying behavior such as data inheritance and cleanup:
import { tap, expect } from '@push.rocks/tapbundle';
import { AsyncContext } from './logcontext.classes.asynccontext.js';
const asyncContext = new AsyncContext();
tap.test('should run scoped and see parent data', async () => {
asyncContext.store.add('parentKey', 'parentValue');
expect(asyncContext.store.get('parentKey')).toEqual('parentValue');
await asyncContext.runScoped(async () => {
asyncContext.store.add('childKey', 'childValue');
expect(asyncContext.store.get('childKey')).toEqual('childValue');
expect(asyncContext.store.get('parentKey')).toEqual('parentValue');
});
});
tap.test('should isolate new keys within a child scope', async () => {
await asyncContext.runScoped(async () => {
asyncContext.store.add('temporaryKey', 'temporaryValue');
expect(asyncContext.store.get('temporaryKey')).toEqual('temporaryValue');
});
// Key won't leak to the parent
expect(asyncContext.store.get('temporaryKey')).toBeUndefined();
});
// More tests ...
tap.start();
Conclusion
The updated runScoped
usage makes child store management more intuitive by dynamically switching asyncContext.store
to a child store inside the scope, then reverting back to the parent store afterwards. This design ensures:
- Scoped Modifications: Data added or removed in a child scope stays in that scope.
- Parent Inheritance: Child scopes still read the parent’s data.
- Easy Syntax: No need to pass the child store around; simply call
asyncContext.store
withinrunScoped
.
With @push.rocks/smartcontext
, you can maintain clear boundaries between different parts of your asynchronous code. This not only enriches your logging with context but also simplifies tracking data across nested or concurrent operations.
License and Legal Information
This repository is under the MIT License. Please note that the MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as necessary for reasonable use in describing the origin of the work.
Trademarks
This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH are trademarks of Task Venture Capital GmbH and are not included within the scope of the MIT license granted herein. Usage must be approved in writing by Task Venture Capital GmbH.
Company Information
Task Venture Capital GmbH
Registered at District Court Bremen HRB 35230 HB, Germany
For any legal inquiries, please contact us at hello@task.vc. By using this repository, you acknowledge that you have read this section and agree to comply with its terms.