fix(core): Updated dependencies and improved AsyncStore debugging and cleanup

This commit is contained in:
Philipp Kunz 2025-01-19 20:06:18 +01:00
parent 8548ad9684
commit 98f6afec7e
8 changed files with 164 additions and 80 deletions

View File

@ -1,5 +1,13 @@
# Changelog
## 2025-01-19 - 2.1.6 - fix(core)
Updated dependencies and improved AsyncStore debugging and cleanup
- Upgraded 'simple-async-context' dependency to version ^0.0.16 for consistency and improvements.
- Added detailed debugging information in AsyncStore when DEBUG environment variable is set.
- Enhanced cleanup process for deleted keys in AsyncStore.
- Removed redundant dependencies from package.json and logcontext.plugins.ts.
## 2025-01-19 - 2.1.5 - fix(dependencies)
Update dependencies for improved compatibility

View File

@ -19,15 +19,11 @@
"@git.zone/tsrun": "^1.2.39",
"@git.zone/tstest": "^1.0.57",
"@push.rocks/smartdelay": "^3.0.5",
"@push.rocks/tapbundle": "^5.0.4",
"@push.rocks/tapbundle": "^5.5.6",
"@types/node": "^22.10.7"
},
"dependencies": {
"@push.rocks/lik": "^6.0.0",
"@push.rocks/smartcls": "^1.0.9",
"@push.rocks/smartunique": "^3.0.3",
"@types/shortid": "2.2.0",
"simple-async-context": "^0.0.15"
"simple-async-context": "^0.0.16"
},
"private": false,
"browserslist": [

59
pnpm-lock.yaml generated
View File

@ -8,21 +8,9 @@ importers:
.:
dependencies:
'@push.rocks/lik':
specifier: ^6.0.0
version: 6.1.0
'@push.rocks/smartcls':
specifier: ^1.0.9
version: 1.0.14
'@push.rocks/smartunique':
specifier: ^3.0.3
version: 3.0.9
'@types/shortid':
specifier: 2.2.0
version: 2.2.0
simple-async-context:
specifier: ^0.0.15
version: 0.0.15
specifier: ^0.0.16
version: 0.0.16
devDependencies:
'@git.zone/tsbuild':
specifier: ^2.1.27
@ -40,8 +28,8 @@ importers:
specifier: ^3.0.5
version: 3.0.5
'@push.rocks/tapbundle':
specifier: ^5.0.4
version: 5.5.4(@aws-sdk/credential-providers@3.731.1)(socks@2.8.3)
specifier: ^5.5.6
version: 5.5.6(@aws-sdk/credential-providers@3.731.1)(socks@2.8.3)
'@types/node':
specifier: ^22.10.7
version: 22.10.7
@ -729,9 +717,6 @@ packages:
'@push.rocks/smartcli@4.0.11':
resolution: {integrity: sha512-KDWfUqWBoUZsOEtsDx36d6qc8GG7Zo5E+HHamYY68KVDO8BMu6jbBucoUUPDksczLEmbXKLmroBP1mn/xozQOA==}
'@push.rocks/smartcls@1.0.14':
resolution: {integrity: sha512-1Sew9ZVTS8mdaKMlOOKZ9uCcSa6QXAfT7yX8xgqF24bXZvyUcf0Lf0d6VvlAMiFJ3bA0H8AsmRFJ8F7HlKW1RA==}
'@push.rocks/smartcrypto@2.0.4':
resolution: {integrity: sha512-1+/5bsjyataf5uUkUNnnVXGRAt+gHVk1KDzozjTqgqJxHvQk1d9fVDohL6CxUhUucTPtu5VR5xNBiV8YCDuGyw==}
@ -825,6 +810,9 @@ packages:
'@push.rocks/smartpromise@4.1.0':
resolution: {integrity: sha512-1E4QZx1bYFMEgbK1C9gb4CB3YRhfkvSeffc5CnT83n7NV4Qly/Sxe9G1Jn0sQBB5+sbFHwTlj/0al5+q4gXiDw==}
'@push.rocks/smartpromise@4.2.0':
resolution: {integrity: sha512-1Yb0u/Yu68D1GPuxPhfMe2MefffqqzK+WmtrCipQl75cBXyaiNiwwrRKaG47ZJquMS+BYxqC/P40cDVAWDvMfw==}
'@push.rocks/smartpuppeteer@2.0.2':
resolution: {integrity: sha512-EcYCT0PX++WjfHp7W5UYX3t8x5gSNpJMMUvhA7SHz8b2t76ItslNWxprRcF0CUQyN1fozbf5StZf7dwdGc/dIA==}
@ -882,8 +870,8 @@ packages:
'@push.rocks/smartyaml@2.0.5':
resolution: {integrity: sha512-tBcf+HaOIfeEsTMwgUZDtZERCxXQyRsWO8Ar5DjBdiSRchbhVGZQEBzXswMS0W5ZoRenjgPK+4tPW3JQGRTfbg==}
'@push.rocks/tapbundle@5.5.4':
resolution: {integrity: sha512-FDL9I95vRENAZmqyQ9/45I1aDaDqFm62rNZOaroqbYX86R7pK75YtwqA0AqQ+QYALX055xw02xlRND5tZmPByQ==}
'@push.rocks/tapbundle@5.5.6':
resolution: {integrity: sha512-V6u+nZwt4fNccxbm3ztZgHr/QAj/uKhaaOUFgtaae0jzYdds4jNEI+mXLpfXuNMgm7Nx93Lk5XUxWKTI8drjNw==}
'@push.rocks/taskbuffer@3.1.7':
resolution: {integrity: sha512-QktGVJPucqQmW/QNGnscf4FAigT1H7JWKFGFdRuDEaOHKFh9qN+PXG3QY7DtZ4jfXdGLxPN4yAufDuPSAJYFnw==}
@ -1455,9 +1443,6 @@ packages:
'@types/serve-static@1.15.7':
resolution: {integrity: sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==}
'@types/shortid@2.2.0':
resolution: {integrity: sha512-jBG2FgBxcaSf0h662YloTGA32M8UtNbnTPekUr/eCmWXq0JWQXgNEQ/P5Gf05Cv66QZtE1Ttr83I1AJBPdzCBg==}
'@types/sinon-chai@3.2.12':
resolution: {integrity: sha512-9y0Gflk3b0+NhQZ/oxGtaAJDvRywCa5sIyaVnounqLvmf93yBF4EgIRspePtkMs3Tr844nCclYMlcCNmLCvjuQ==}
@ -3643,8 +3628,8 @@ packages:
resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
engines: {node: '>=14'}
simple-async-context@0.0.15:
resolution: {integrity: sha512-NJKVyA89zo0LVAPy2AjeVndjh/gWQhI9F2mnZvhQSbxithPJpbKOd6YA5VOsy90QIE7TV3bhzHNpod2EgMCmWw==}
simple-async-context@0.0.16:
resolution: {integrity: sha512-FbE32PwJcZQU+rta11BGI5K+bSwPf2FPnkMmOJVWUOz5qRMgwEqRRc2e85fhkRkYe3Pb9P/r5ELWjgaBGBsnDQ==}
simple-swizzle@0.2.2:
resolution: {integrity: sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo=}
@ -5040,7 +5025,7 @@ snapshots:
'@push.rocks/smartlog': 3.0.7
'@push.rocks/smartpromise': 4.1.0
'@push.rocks/smartshell': 3.2.2
'@push.rocks/tapbundle': 5.5.4(@aws-sdk/credential-providers@3.731.1)(socks@2.8.3)
'@push.rocks/tapbundle': 5.5.6(@aws-sdk/credential-providers@3.731.1)(socks@2.8.3)
'@types/ws': 8.5.13
figures: 6.1.0
ws: 8.18.0
@ -5335,11 +5320,9 @@ snapshots:
'@push.rocks/smartrx': 3.0.7
yargs-parser: 21.1.1
'@push.rocks/smartcls@1.0.14': {}
'@push.rocks/smartcrypto@2.0.4':
dependencies:
'@push.rocks/smartpromise': 4.1.0
'@push.rocks/smartpromise': 4.2.0
'@types/node-forge': 1.3.11
node-forge: 1.3.1
@ -5349,7 +5332,7 @@ snapshots:
'@push.rocks/smartdelay': 3.0.5
'@push.rocks/smartlog': 3.0.7
'@push.rocks/smartmongo': 2.0.10(@aws-sdk/credential-providers@3.731.1)(socks@2.8.3)
'@push.rocks/smartpromise': 4.1.0
'@push.rocks/smartpromise': 4.2.0
'@push.rocks/smartrx': 3.0.7
'@push.rocks/smartstring': 4.0.15
'@push.rocks/smarttime': 4.1.1
@ -5386,7 +5369,7 @@ snapshots:
'@push.rocks/smartexpect@1.4.0':
dependencies:
'@push.rocks/smartdelay': 3.0.5
'@push.rocks/smartpromise': 4.1.0
'@push.rocks/smartpromise': 4.2.0
fast-deep-equal: 3.1.3
'@push.rocks/smartfeed@1.0.11':
@ -5511,7 +5494,7 @@ snapshots:
'@push.rocks/mongodump': 1.0.8
'@push.rocks/smartdata': 5.2.10(@aws-sdk/credential-providers@3.731.1)(socks@2.8.3)
'@push.rocks/smartpath': 5.0.18
'@push.rocks/smartpromise': 4.1.0
'@push.rocks/smartpromise': 4.2.0
mongodb-memory-server: 8.16.1
transitivePeerDependencies:
- '@aws-sdk/credential-providers'
@ -5593,6 +5576,8 @@ snapshots:
'@push.rocks/smartpromise@4.1.0': {}
'@push.rocks/smartpromise@4.2.0': {}
'@push.rocks/smartpuppeteer@2.0.2':
dependencies:
'@pushrocks/smartdelay': 2.0.13
@ -5754,7 +5739,7 @@ snapshots:
'@types/js-yaml': 3.12.10
js-yaml: 3.14.1
'@push.rocks/tapbundle@5.5.4(@aws-sdk/credential-providers@3.731.1)(socks@2.8.3)':
'@push.rocks/tapbundle@5.5.6(@aws-sdk/credential-providers@3.731.1)(socks@2.8.3)':
dependencies:
'@open-wc/testing': 4.0.0
'@push.rocks/consolecolor': 2.0.2
@ -5767,7 +5752,7 @@ snapshots:
'@push.rocks/smartjson': 5.0.20
'@push.rocks/smartmongo': 2.0.10(@aws-sdk/credential-providers@3.731.1)(socks@2.8.3)
'@push.rocks/smartpath': 5.0.18
'@push.rocks/smartpromise': 4.1.0
'@push.rocks/smartpromise': 4.2.0
'@push.rocks/smartrequest': 2.0.23
'@push.rocks/smarts3': 2.2.5
'@push.rocks/smartshell': 3.2.2
@ -6601,8 +6586,6 @@ snapshots:
'@types/node': 22.10.7
'@types/send': 0.17.4
'@types/shortid@2.2.0': {}
'@types/sinon-chai@3.2.12':
dependencies:
'@types/chai': 5.0.1
@ -9190,7 +9173,7 @@ snapshots:
signal-exit@4.1.0: {}
simple-async-context@0.0.15: {}
simple-async-context@0.0.16: {}
simple-swizzle@0.2.2:
dependencies:

View File

@ -2,6 +2,8 @@ import { tap, expect } from '@push.rocks/tapbundle';
import { AsyncContext } from '../ts/logcontext.classes.asynccontext.js';
import { AsyncStore } from '../ts/logcontext.classes.asyncstore.js';
process.env.DEBUG = 'true';
/**
* This test file demonstrates how to use the AsyncContext and ensures
* that runScoped() properly creates child AsyncStore contexts and merges parent data.
@ -35,15 +37,28 @@ tap.test('should not contaminate the parent store with child-only data', async (
expect(asyncContext.store.get('temporaryKey')).toBeUndefined();
});
tap.test('should allow adding data in multiple scopes independently', async () => {
tap.test('should allow adding data in multiple scopes independently', async (toolsArg) => {
const done = toolsArg.cumulativeDefer();
// add data in first scope
await asyncContext.runScoped(async () => {
asyncContext.store.add('childKey1', 'childValue1');
expect(asyncContext.store.get('childKey1')).toEqual('childValue1');
asyncContext.runScoped(async () => {
const subDone = done.subDefer();
asyncContext.store.add('childKey1', 'childValue1-v1');
await toolsArg.delayFor(2000);
expect(asyncContext.store.get('childKey1')).toEqual('childValue1-v1');
subDone.resolve();
});
asyncContext.runScoped(async () => {
const subDone = done.subDefer();
asyncContext.store.add('childKey1', 'childValue1-v2');
await toolsArg.delayFor(1000);
expect(asyncContext.store.get('childKey1')).toEqual('childValue1-v2');
subDone.resolve();
});
// add data in second scope
await asyncContext.runScoped(async () => {
asyncContext.runScoped(async () => {
asyncContext.store.add('childKey2', 'childValue2');
expect(asyncContext.store.get('childKey2')).toEqual('childValue2');
});
@ -51,23 +66,27 @@ tap.test('should allow adding data in multiple scopes independently', async () =
// neither childKey1 nor childKey2 should exist in the parent store
expect(asyncContext.store.get('childKey1')).toBeUndefined();
expect(asyncContext.store.get('childKey2')).toBeUndefined();
await done.promise;
});
tap.test('should allow deleting data in a child store without removing it from the parent store', async () => {
// ensure parent has some data
asyncContext.store.add('deletableKey', 'iShouldStayInParent');
tap.test(
'should allow deleting data in a child store without removing it from the parent store',
async (toolsArg) => {
// ensure parent has some data
asyncContext.store.add('deletableKey', 'iShouldStayInParent');
await asyncContext.runScoped(async () => {
// child sees the parent's data
await asyncContext.runScoped(async () => {
// child sees the parent's data
expect(asyncContext.store.get('deletableKey')).toEqual('iShouldStayInParent');
// attempt to delete it in the child
asyncContext.store.delete('deletableKey');
// child no longer sees it
expect(asyncContext.store.get('deletableKey')).toBeUndefined();
// but parent still has it
});
expect(asyncContext.store.get('deletableKey')).toEqual('iShouldStayInParent');
// attempt to delete it in the child
asyncContext.store.delete('deletableKey');
// child no longer sees it
expect(asyncContext.store.get('deletableKey')).toBeUndefined();
// but parent still has it
});
expect(asyncContext.store.get('deletableKey')).toEqual('iShouldStayInParent');
});
}
);
tap.test('should allow multiple child scopes to share the same parent store data', async () => {
// add a key to the parent store
@ -85,4 +104,4 @@ tap.test('should allow multiple child scopes to share the same parent store data
});
});
export default tap.start();
export default tap.start();

View File

@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: '@push.rocks/smartcontext',
version: '2.1.5',
version: '2.1.6',
description: 'A module providing advanced asynchronous context management to enrich logs with context and manage scope effectively in Node.js applications.'
}

View File

@ -11,8 +11,7 @@ export class AsyncContext {
this._store = value;
}
public async runScoped(functionArg: () => Promise<void>) {
const childStore = new AsyncStore(this.store);
await this._context.run(childStore, async () => {
await this._context.run(new AsyncStore(this.store), async () => {
await functionArg()
});
}

View File

@ -1,50 +1,135 @@
import * as plugins from './logcontext.plugins.js';
export class AsyncStore {
private static idCounter = 0;
private id: number;
private parentStore?: AsyncStore;
private deletedKeys: string[] = [];
private dataObject: {[key: string]: any} = {};
private dataObject: { [key: string]: any } = {};
constructor(parentStore?: AsyncStore) {
this.parentStore = parentStore;
this.id = AsyncStore.idCounter++;
}
/**
* Logs debug info if process.env.DEBUG is set.
*/
private logDebug(functionName: string, before: Record<string, any>, after: Record<string, any>) {
if (process.env.DEBUG) {
console.log(`Store ID: ${this.id}`);
console.log(`Function: ${functionName}`);
console.log('--- Before ---');
console.log(before);
console.log('--- After ---');
console.log(after);
console.log('-----------------------');
}
}
/**
* Cleans up the deleted keys if they no longer exist in any parent store.
*/
private cleanUp() {
for (const key of this.deletedKeys) {
if (this.parentStore && this.parentStore.get(key)) {
// ok still valid
// Parent still has it, so keep in deletedKeys
} else {
delete this.deletedKeys[key];
const index = this.deletedKeys.indexOf(key);
if (index !== -1) {
this.deletedKeys.splice(index, 1);
}
}
}
}
/**
* Adds or updates a value under a specific key in this store.
*/
public add(keyArg: string, objectArg: any) {
// capture the before state
const before = { ...this.dataObject, deletedKeys: [...this.deletedKeys] };
this.cleanUp();
// If this key was previously deleted, remove it from deletedKeys.
if (this.deletedKeys.includes(keyArg)) {
this.deletedKeys = this.deletedKeys.filter((key) => key !== keyArg);
}
this.dataObject[keyArg] = objectArg;
// capture the after state
const after = { ...this.dataObject, deletedKeys: [...this.deletedKeys] };
this.logDebug('add', before, after);
}
/**
* Deletes a key from the current store.
* If a parent store has the key, we record it in `deletedKeys` so the child store "shadows" it.
*/
public delete(paramName: string) {
// capture the before state
const before = { ...this.dataObject, deletedKeys: [...this.deletedKeys] };
this.cleanUp();
if (this.parentStore.get(paramName)) {
if (this.parentStore?.get(paramName)) {
// The parent store has this key; let's mark it as deleted in the child
this.deletedKeys.push(paramName);
}
delete this.dataObject[paramName];
// capture the after state
const after = { ...this.dataObject, deletedKeys: [...this.deletedKeys] };
this.logDebug('delete', before, after);
}
/**
* Gets the value of a key, checking this store first, then the parent store if necessary.
* Will log the store state before/after for debugging.
*/
public get(paramName: string) {
// capture the before state
const before = { ...this.dataObject, deletedKeys: [...this.deletedKeys] };
this.cleanUp();
// figure out if paramName is deleted or present
let result: any;
if (this.deletedKeys.includes(paramName)) {
return undefined;
result = undefined;
} else {
result = this.dataObject[paramName] ?? this.parentStore?.get(paramName);
}
return this.dataObject[paramName] || this.parentStore?.get(paramName);
// capture the after state; we can also show the `result` in the log
const after = {
...this.dataObject,
deletedKeys: [...this.deletedKeys],
retrievedKey: paramName,
result
};
this.logDebug('get', before, after);
return result;
}
/**
* Returns all keys and values, merged with the parent store, but
* does NOT include keys that are "deleted" in the child.
* Child store should override parent if the same key exists in both.
*/
public getAll() {
this.cleanUp();
return {...this.dataObject, ...(this.parentStore?.getAll() || {})};
// first, get parent's data as a shallow copy
const parentData = { ...(this.parentStore?.getAll() || {}) };
// remove keys from parent data that this child has deleted
for (const key of this.deletedKeys) {
delete parentData[key];
}
// child's data overrides parent data for any matching keys
return {
...parentData,
...this.dataObject
};
}
}
}

View File

@ -1,9 +1,3 @@
// pushrocks scope
import * as lik from '@push.rocks/lik';
import * as smartunique from '@push.rocks/smartunique';
export { lik, smartunique };
// third party scope
import simpleAsyncContext from 'simple-async-context';