smartcontext/readme.md

181 lines
7.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# @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:
```bash
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:
```typescript
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:
```typescript
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
```typescript
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:
```typescript
// 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.
```typescript
// 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.
```typescript
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](https://www.npmjs.com/package/@push.rocks/tapbundle). It demonstrates `runScoped()` creating isolated child stores and verifying behavior such as data inheritance and cleanup:
```typescript
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.
## License and Legal Information
This repository is under the [MIT License](./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.