diff --git a/changelog.md b/changelog.md index ad7837a..3fc978b 100644 --- a/changelog.md +++ b/changelog.md @@ -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 diff --git a/package.json b/package.json index 755cd85..bf655a2 100644 --- a/package.json +++ b/package.json @@ -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": [ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bf73fa6..efcd44b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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: diff --git a/test/test.both.ts b/test/test.ts similarity index 67% rename from test/test.both.ts rename to test/test.ts index 08968be..d7f2cf3 100644 --- a/test/test.both.ts +++ b/test/test.ts @@ -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(); \ No newline at end of file +export default tap.start(); diff --git a/ts/00_commitinfo_data.ts b/ts/00_commitinfo_data.ts index 464df7b..84598c9 100644 --- a/ts/00_commitinfo_data.ts +++ b/ts/00_commitinfo_data.ts @@ -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.' } diff --git a/ts/logcontext.classes.asynccontext.ts b/ts/logcontext.classes.asynccontext.ts index b5dbc34..01a0249 100644 --- a/ts/logcontext.classes.asynccontext.ts +++ b/ts/logcontext.classes.asynccontext.ts @@ -11,8 +11,7 @@ export class AsyncContext { this._store = value; } public async runScoped(functionArg: () => Promise) { - const childStore = new AsyncStore(this.store); - await this._context.run(childStore, async () => { + await this._context.run(new AsyncStore(this.store), async () => { await functionArg() }); } diff --git a/ts/logcontext.classes.asyncstore.ts b/ts/logcontext.classes.asyncstore.ts index 778af50..0c67616 100644 --- a/ts/logcontext.classes.asyncstore.ts +++ b/ts/logcontext.classes.asyncstore.ts @@ -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, after: Record) { + 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 + }; } -} +} \ No newline at end of file diff --git a/ts/logcontext.plugins.ts b/ts/logcontext.plugins.ts index b2e2c5d..389a1bc 100644 --- a/ts/logcontext.plugins.ts +++ b/ts/logcontext.plugins.ts @@ -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';