@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
  1. Child Store Access: Within the scope, asyncContext.store points to a child store.
  2. Parent Data: Any keys from the parent are still visible in the child (unless overridden).
  3. No Cross-Contamination: Keys added in the child store dont 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 wont 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 dont overwrite or “contaminate” each others 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 thats 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 parents data.
  • Easy Syntax: No need to pass the child store around; simply call asyncContext.store within runScoped.

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.

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.

Description
A module to enrich logs with context, featuring async log contexts and scope management.
Readme 797 KiB
Languages
TypeScript 100%