Skip to content

Commit 8f21e77

Browse files
committed
Add Jest configuration and update package.json dependencies
1 parent a8cd019 commit 8f21e77

File tree

9 files changed

+216
-29
lines changed

9 files changed

+216
-29
lines changed

.vscode/launch.json

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{
2+
"configurations": [
3+
{
4+
"name": "Jest file",
5+
"type": "node",
6+
"request": "launch",
7+
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/jest",
8+
"args": [
9+
"${fileBasenameNoExtension}",
10+
"--runInBand",
11+
"--watch",
12+
"--coverage=false",
13+
"--no-cache"
14+
],
15+
"cwd": "${workspaceRoot}",
16+
"console": "integratedTerminal",
17+
"internalConsoleOptions": "openOnFirstSessionStart",
18+
"sourceMaps": true,
19+
"windows": {
20+
"program": "${workspaceFolder}/node_modules/jest/bin/jest"
21+
}
22+
}
23+
]
24+
}

jest.config.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,6 @@ module.exports = {
99
},
1010
setupFilesAfterEnv:[
1111
"./jest.setupFilesAfterEnv.ts"
12-
]
12+
],
13+
testTimeout: 30000,
1314
};

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
"jest-extended": "^4.0.2",
1515
"ts-jest": "^29.1.2",
1616
"ts-node": "^10.9.2",
17+
"ts-node-dev": "^2.0.0",
1718
"typescript": "^5.4.5"
1819
},
1920
"keywords": [],

src/TryCatchFinallyHooks.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ export class TryCatchFinallyHooksBuilder<THookContext extends {}, TDecoratorArgs
4848

4949
private hooks:ITryCatchFinallyHook<any>[] = []
5050

51-
add<NewHookContext, NewArgs = DecoratorArgsOf<NewHookContext>>(hook: ITryCatchFinallyHook<NewHookContext>)
51+
add<NewHookContext, NewArgs = DecoratorArgsOf<NewHookContext>>(hook: ITryCatchFinallyHook<THookContext & NewHookContext>)
5252
: TryCatchFinallyHooksBuilder<THookContext & NewHookContext, TDecoratorArgs & NewArgs>
5353
{
5454
this.hooks.push(hook);
@@ -75,8 +75,8 @@ export class TryCatchFinallyHooksBuilder<THookContext extends {}, TDecoratorArgs
7575
const afterHooksTry = this.afterHooksTry?.bind(this)
7676
return (func:any)=>{
7777
return createTryCatchFinally(func, {
78-
onTry() {
79-
const ctx = { args }
78+
onTry(funcArgs) {
79+
const ctx = { funcArgs, args }
8080
const bht = beforeHooksTry(ctx as any)
8181
const hooksRes = _this.forEachHook(hook=> hook.onTry( ctx as any))
8282
for (const hookRes of [...hooksRes]) {

src/callStack.ts

Lines changed: 0 additions & 17 deletions
This file was deleted.

src/example/tracker.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { callStack } from "../callStack";
1+
import { callStack } from "../node/callStack";
22
import { logOnFinally } from "./logOnFinally";
33
import { measureDuration } from "../measureDuration";
44
import { TryCatchFinallyHooksBuilder, ContextOf } from "../TryCatchFinallyHooks";

src/node/callStack.test.ts

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
import { TryCatchFinallyHooksBuilder } from "../TryCatchFinallyHooks"
2+
import { callStack } from './callStack'
3+
import { AsyncResource, AsyncLocalStorage } from 'node:async_hooks'
4+
5+
function createTrack(log:any){
6+
return new TryCatchFinallyHooksBuilder()
7+
.add(callStack)
8+
.add({
9+
onTry(ctx) {
10+
log("onTry",ctx.args.name)
11+
return {
12+
onFinally() {
13+
log("onFinally",ctx.callstack.map(c=>c.args.name).join("/"))
14+
},
15+
onCatch() {
16+
log("onCatch",ctx.callstack.map(c=>c.args.name).join("/"))
17+
}
18+
}
19+
}
20+
})
21+
}
22+
23+
24+
test("callstack", ()=>{
25+
const log = jest.fn((...args:any[])=>console.log(...args))
26+
const track = createTrack(log)
27+
28+
const myChildFunc = jest.fn(track.asFunctionWrapper({name:"MyChildFunc"})((a:number,b:number)=>{
29+
return a+b
30+
}))
31+
32+
33+
const myParentFunc = jest.fn(track.asFunctionWrapper({name: 'MyParentFunc'})(()=>{
34+
(new Array(10).fill(0).map((_,i)=>{
35+
return myChildFunc(i,i*2)
36+
}))
37+
}))
38+
39+
40+
myParentFunc()
41+
42+
expect(myChildFunc).toHaveBeenCalledTimes(10)
43+
44+
expect(log).toHaveBeenCalledTimes(22)
45+
expect(log).toHaveBeenCalledWith("onTry", "MyParentFunc")
46+
expect(log).toHaveBeenCalledWith("onTry", "MyParentFunc/MyChildFunc")
47+
})
48+
49+
test("callstack async",async ()=>{
50+
const log = jest.fn((...args:any[])=>console.log("async",...args))
51+
const track = createTrack(log)
52+
53+
const asyncStr = new AsyncLocalStorage<any>()
54+
asyncStr.enterWith(["root"])
55+
56+
const myChildFunc = jest.fn(track.asFunctionWrapper({name:"MyAsyncChildFunc"})(async (a:number,b:number)=>{
57+
const path = [...asyncStr.getStore()||[]]
58+
asyncStr.enterWith([...path,"child"])
59+
await delay(Math.random()*1000)
60+
asyncStr.enterWith(path)
61+
62+
return a+b
63+
}))
64+
65+
66+
const myParentFunc = jest.fn(track.asFunctionWrapper({name: 'MyAsyncParentFunc'})(()=>{
67+
const path = [...asyncStr.getStore()||[]]
68+
asyncStr.enterWith([...path,"parent"])
69+
return Promise.allSettled(new Array(2).fill(0).map(async (_,i)=>{
70+
await myChildFunc(i,i*2)
71+
}))
72+
}))
73+
74+
75+
await myParentFunc()
76+
77+
expect(myChildFunc).toHaveBeenCalledTimes(10)
78+
79+
expect(log).toHaveBeenCalledTimes(22)
80+
expect(log).toHaveBeenCalledWith("onTry", "MyAsyncParentFunc")
81+
expect(log).toHaveBeenCalledWith("onTry", "MyAsyncParentFunc/MyAsyncChildFunc")
82+
})
83+
84+
function delay(ms:number){return new Promise(r=>setTimeout(r,ms))}
85+
86+
87+
test('async hooks',async ()=>{
88+
const callStack = new AsyncLocalStorage<{name:string}[]>()
89+
callStack.enterWith([{name:"parent"}])
90+
await delay(100)
91+
expect(callStack.getStore()).toEqual([{name:"parent"}])
92+
callStack.enterWith([{name:"parent"}, {name:'child'}])
93+
await delay(100)
94+
expect(callStack.getStore()).toEqual([{name:"parent"},{name:"child"}])
95+
await delay(100)
96+
97+
98+
async function myAsyncFunc(){
99+
await delay(500)
100+
expect(callStack.getStore()).toEqual([{name:"parent"},{name:"child"}])
101+
callStack.enterWith([...callStack.getStore()!, {name:"myAsyncFunc"}])
102+
await delay(1000)
103+
expect(callStack.getStore()).toEqual([{name:"parent"},{name:"child"}, {name:"myAsyncFunc"}])
104+
}
105+
106+
async function myAsyncFunc2(){
107+
await delay(1000)
108+
expect(callStack.getStore()).toEqual([{name:"parent"},{name:"child"}])
109+
callStack.enterWith([...callStack.getStore()!, {name:"myAsyncFunc2"}])
110+
await delay(1000)
111+
expect(callStack.getStore()).toEqual([{name:"parent"},{name:"child"}, {name:"myAsyncFunc2"}])
112+
}
113+
114+
115+
await Promise.allSettled([myAsyncFunc(), myAsyncFunc2()])
116+
117+
expect(callStack.getStore()).toEqual([{name:"parent"},{name:"child"}])
118+
119+
120+
})
121+
122+
test.only('async hooks array',async ()=>{
123+
const callStack = new AsyncLocalStorage<string[]>()
124+
callStack.enterWith(["parent"])
125+
await delay(100)
126+
expect(callStack.getStore()).toEqual(["parent"])
127+
await delay(100)
128+
129+
130+
const stack = {} as any
131+
Error.captureStackTrace(stack)
132+
console.log("stack",stack.stack)
133+
134+
135+
async function myAsyncFunc(n: number){
136+
const actionName = "myAsyncFunc"+n
137+
//await delay(0)
138+
const storeBeforeAwait = callStack.getStore()!
139+
//expect(storeBeforeAwait).toEqual(["parent"])
140+
callStack.enterWith([...storeBeforeAwait, actionName])
141+
142+
await delay(Math.random()*1000)
143+
144+
const storeAfterAwait = callStack.getStore()!
145+
expect(storeAfterAwait).toEqual([...storeBeforeAwait,actionName])
146+
const newStack = [...storeAfterAwait]
147+
newStack.pop()
148+
callStack.enterWith(newStack)
149+
}
150+
151+
152+
await Promise.all(new Array(2).fill(0).map((_,i)=>myAsyncFunc(i)))
153+
154+
expect(callStack.getStore()).toEqual(["parent"])
155+
156+
157+
})

src/node/callStack.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { ITryCatchFinallyHook } from "../TryCatchFinallyHooks";
2+
import {AsyncLocalStorage} from 'async_hooks'
3+
4+
5+
type CallstackContext = { args: { name: string; }; callstack: CallstackContext[]; };
6+
let globalCallstack: AsyncLocalStorage<CallstackContext[]> = new AsyncLocalStorage();
7+
export const callStack: ITryCatchFinallyHook<CallstackContext> = {
8+
onTry(ctx) {
9+
const prevCallstack = [...globalCallstack.getStore()||[]];
10+
11+
ctx.callstack = [...prevCallstack, ctx];
12+
globalCallstack.enterWith(ctx.callstack);
13+
return {
14+
onFinally() {
15+
globalCallstack.enterWith(prevCallstack);
16+
},
17+
lastInQueue: true
18+
};
19+
}
20+
};

src/tryCatchFinally.ts

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
export type FunctionContext<TFunc extends (...args: any[]) => any = (...args: any[]) => any> = {
2+
funcArgs: Parameters<TFunc>
23
outcome?: FunctionExecutionOutcome<TFunc>
34
}
45

5-
export type FunctionInterceptors<TFunc extends (...args: any[]) => any = (...args:any[])=>any, TContext extends {} = {}> = {
6-
onTry: () => {
6+
export type FunctionInterceptors<TFunc extends (this:any, ...args: any[]) => any = (...args:any[])=>any, TContext extends {} = {}> = {
7+
onTry: (funcArgs:Parameters<TFunc>) => {
78
context?: TContext
89
onCatch?: (ctx: TContext & FunctionContext<TFunc>) => void
910
onFinally?: (ctx: TContext & FunctionContext<TFunc>) => void
@@ -17,22 +18,22 @@ export function createTryCatchFinally<TFunc extends (this:any, ...args: any[]) =
1718
interceptors: FunctionInterceptors<TFunc, TContext>
1819
): TFunc {
1920
type Ctx = TContext & FunctionContext<TFunc>
20-
return function (...args) {
21+
return function (...funcArgs) {
2122
let isAsync = false
2223
let funcRes: ReturnType<TFunc>;
23-
let ctx: Ctx = {} as any
24+
let ctx: Ctx = { funcArgs } as any
2425
let tryRes: ReturnType<NonNullable<typeof interceptors.onTry>> = {} as any;
2526
let onCatch = 'onCatch' in interceptors ? interceptors.onCatch : undefined;
2627
let onFinally = 'onFinally' in interceptors ? interceptors.onFinally : undefined;
2728

2829
try {
2930
if(interceptors.onTry)
30-
tryRes = interceptors.onTry()
31-
if(tryRes.context) ctx = tryRes.context
31+
tryRes = interceptors.onTry(funcArgs as any)
32+
if(tryRes.context) ctx = Object.assign(tryRes.context, ctx)
3233
if('onCatch' in tryRes) onCatch = tryRes.onCatch
3334
if('onFinally' in tryRes) onFinally = tryRes.onFinally
3435

35-
funcRes = fn.apply(this,args);
36+
funcRes = fn.apply(this,funcArgs);
3637
isAsync = isPromise(funcRes);
3738
ctx.outcome = { type: 'success', result: funcRes as Awaited<ReturnType<TFunc>> };
3839
if (!isAsync) {

0 commit comments

Comments
 (0)