├── .gitignore ├── .node-version ├── .prettierignore ├── .vscode ├── launch.json └── settings.json ├── LICENSE ├── README.md ├── assets └── illustration.png ├── jest.config.js ├── package.json ├── scripts └── playground.ts ├── src ├── index.ts ├── lib │ ├── AsyncContext.ts │ ├── AsyncSnapshot.ts │ ├── AsyncVariable.ts │ └── utils │ │ └── runInFork.ts ├── polyfill │ ├── AsyncStack.ts │ ├── Events.ts │ ├── Polyfill.ts │ ├── Promise.ts │ └── _lib.ts └── tests │ ├── _lib.ts │ ├── async.test.ts │ ├── error.test.ts │ ├── misc.test.ts │ ├── snapshot.test.ts │ ├── sync.test.ts │ ├── tc39.test.ts │ └── timers.test.ts ├── tsconfig.build.json ├── tsconfig.json └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | .DS_Store 3 | coverage 4 | node_modules 5 | dist 6 | build 7 | tsconfig.tsbuildinfo -------------------------------------------------------------------------------- /.node-version: -------------------------------------------------------------------------------- 1 | 21.6.1 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | README.md -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "configurations": [ 3 | { 4 | "type": "node", 5 | "name": "vscode-jest-tests", 6 | "request": "launch", 7 | "console": "integratedTerminal", 8 | "internalConsoleOptions": "neverOpen", 9 | "disableOptimisticBPs": true, 10 | "program": "${workspaceFolder}/node_modules/.bin/jest", 11 | "cwd": "${workspaceFolder}", 12 | "args": [ 13 | "--runInBand", 14 | "--watchAll=false" 15 | ] 16 | } 17 | ] 18 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "jest.jestCommandLine": "jest" 3 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2025 Ilias Bhallil 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | ![Illustration](https://github.com/iliasbhal/simple-async-context/blob/main/assets/illustration.png?raw=true) 3 | 4 | 5 | ![NPM License](https://img.shields.io/npm/l/simple-async-context) 6 | 7 | ## ✨ Simple-Async-Context 8 | Polyfill implementing the [TC39 proposal for AsyncContext](https://github.com/tc39/proposal-async-context) in Javascript. 9 | 10 | ## 💼 How to install? 11 | 12 | ```sh 13 | $ npm i simple-async-context 14 | ``` 15 | ```sh 16 | $ yarn add simple-async-context 17 | ``` 18 | 19 | ## 💪 Motivation 20 | Promises and async/await syntax improved the ergonomics of writing asynchronous JavaScript. It allows developers to think of asynchronous code in terms of synchronous code. The behavior of the event loop executing the code remains the same in an asynchronous block. However, the event loop loses implicit information about the call site. 21 | 22 | Knowing the call site of a function is crucial for a variety of purposes. For example, it allows for: (non-exhaustive list) 23 | - Attribution of side effects in software. 24 | - Tracing tools and profilers to analytize the code. 25 | - Sharing data between nested functions calls without "prop drilling" 26 | 27 | That's why the feature and this polyfill are useful 🙃. 28 | 29 | ## 📚 How To Use? 30 | 31 | 1. Make sure that your code is compiled to remove native async/await. The simplest is to target `ES6` in your `tsconfig.json`. But other tools can do the trick as well. 32 | 33 | ```tsx 34 | // tsconfig.json 35 | { 36 | "compilerOptions": { 37 | "target": "ES6" 38 | } 39 | } 40 | ``` 41 | 42 | 43 | 2. An image is worth a thousand words. 44 | Please check the code below 🫡 45 | 46 | ```tsx 47 | import { AsyncContext } from 'simple-async-context'; 48 | 49 | const context = new AsyncContext.Variable(); 50 | 51 | const wait = (timeout: number) => new Promise(r => setTimeout(r, timeout)); 52 | const randomTimeout = () => Math.random() * 1000; 53 | 54 | async function main() { 55 | 56 | context.get(); // => 'top' 57 | 58 | await wait(randomTimeout()); 59 | 60 | context.run("A", () => { 61 | context.get(); // => 'A' 62 | 63 | setTimeout(() => { 64 | context.get(); // => 'A' 65 | }, randomTimeout()); 66 | 67 | context.run("B", async () => { // contexts can be nested. 68 | context.get(); // => 'B' 69 | 70 | await wait(randomTimeout()); 71 | 72 | context.get(); // => 'B' 73 | 74 | await wait(randomTimeout()); 75 | 76 | context.get(); // => 'B' // contexts are restored 77 | 78 | setTimeout(() => { 79 | context.get(); // => 'B' 80 | }, randomTimeout()); 81 | }); 82 | 83 | 84 | context.run("C", async () => { // contexts can be nested. 85 | context.get(); // => 'C' 86 | 87 | await wait(randomTimeout()); 88 | 89 | context.get(); // => 'C' 90 | 91 | await wait(randomTimeout()); 92 | 93 | context.get(); // => 'C' 94 | 95 | setTimeout(() => { 96 | context.get(); // => 'C' 97 | }, randomTimeout()); 98 | }); 99 | 100 | }); 101 | 102 | await wait(randomTimeout()); 103 | 104 | context.get(); // => 'top' 105 | } 106 | 107 | context.run("top", main); 108 | 109 | ``` 110 | 111 | ## Snapshot 112 | 113 | // TODO 114 | 115 | ## Tracking 116 | 117 | // TODO 118 | 119 | 120 | ## :book: License 121 | 122 | The MIT License 123 | 124 | Copyright (c) 2025 Ilias Bhallil 125 | 126 | Permission is hereby granted, free of charge, to any person obtaining a copy 127 | of this software and associated documentation files (the "Software"), to deal 128 | in the Software without restriction, including without limitation the rights 129 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 130 | copies of the Software, and to permit persons to whom the Software is 131 | furnished to do so, subject to the following conditions: 132 | 133 | The above copyright notice and this permission notice shall be included in all 134 | copies or substantial portions of the Software. 135 | 136 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 137 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 138 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 139 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 140 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 141 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 142 | SOFTWARE. 143 | -------------------------------------------------------------------------------- /assets/illustration.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iliasbhal/simple-async-context/2e4536cc46c098c71b9070cdd669a3ccfa452068/assets/illustration.png -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | verbose: true, 3 | transform: { 4 | '^.+\\.(ts|tsx|js|jsx)$': 'ts-jest', 5 | }, 6 | testPathIgnorePatterns: [ 7 | './build', 8 | ], 9 | coveragePathIgnorePatterns: [ 10 | '.d.ts$', 11 | ], 12 | }; 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0.4", 3 | "name": "simple-async-context", 4 | "description": "Async context for node and the browser", 5 | "main": "build/index.js", 6 | "typings": "build/index.d.ts", 7 | "source": "src/index.ts", 8 | "files": [ 9 | "build/*" 10 | ], 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/iliasbhal/simple-async-context.git" 14 | }, 15 | "keywords": [], 16 | "author": "Ilias Bhallil ", 17 | "license": "MIT", 18 | "bugs": { 19 | "url": "https://github.com/iliasbhal/simple-async-context/issues" 20 | }, 21 | "homepage": "https://github.com/iliasbhal/simple-async-context", 22 | "scripts": { 23 | "dev": "yarn setup && yarn build:watch", 24 | "setup": "yarn build && yarn sync", 25 | "sync": "npm link && npm link simple-async-context", 26 | "deploy": "yarn test &&yarn yarn build && npm publish", 27 | "lint": "prettier --write ./src", 28 | "test": "jest", 29 | "build": "rm -rf ./build && tsc --project ./tsconfig.build.json", 30 | "build:watch": "yarn build:ts -w & yarn build:dts -w", 31 | "prepublish": "yarn test && yarn build", 32 | "playground": "bun run --watch ./scripts/playground.ts" 33 | }, 34 | "peerDependencies": {}, 35 | "dependencies": {}, 36 | "devDependencies": { 37 | "@babel/cli": "^7.19.3", 38 | "@babel/core": "^7.23.9", 39 | "@babel/plugin-external-helpers": "^7.22.5", 40 | "@babel/plugin-proposal-decorators": "^7.20.2", 41 | "@babel/plugin-proposal-explicit-resource-management": "^7.23.9", 42 | "@babel/plugin-transform-runtime": "^7.22.10", 43 | "@babel/preset-env": "^7.19.4", 44 | "@babel/preset-react": "^7.18.6", 45 | "@babel/preset-typescript": "^7.23.3", 46 | "@jest/types": "^28.1.3", 47 | "@types/bun": "^1.1.14", 48 | "@types/jest": "^28.1.6", 49 | "@types/jsdom": "^20.0.1", 50 | "@types/node": "^18.6.4", 51 | "babel-jest": "^29.2.2", 52 | "babel-preset-minify": "^0.5.2", 53 | "bun": "^1.1.42", 54 | "jest": "^28.1.3", 55 | "prettier": "^3.4.2", 56 | "ts-jest": "^29.2.5", 57 | "tslib": "^2.6.2", 58 | "tsx": "^4.19.2", 59 | "typescript": "^5.3.3", 60 | "wait": "^0.4.2" 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /scripts/playground.ts: -------------------------------------------------------------------------------- 1 | import { AsyncContext } from '../src'; 2 | import { Polyfill } from '../src/polyfill/Polyfill'; 3 | 4 | const context = new AsyncContext.Variable(); 5 | const wait = (timeout) => new Promise(r => Polyfill.originalSetTimeout(r, timeout)); 6 | 7 | const randomTimeout = () => Math.random() * 300; 8 | 9 | export const example = async () => { 10 | 11 | 12 | console.log(context.get() === 'top') 13 | 14 | await wait(randomTimeout()); 15 | 16 | context.run("A", () => { 17 | console.log(context.get() === 'A') 18 | 19 | setTimeout(() => { 20 | console.log(context.get() === 'A') 21 | }, randomTimeout()); 22 | 23 | context.run("B", async () => { // contexts can be nested. 24 | await wait(randomTimeout()); 25 | 26 | console.log(context.get() === 'B') 27 | 28 | console.log(context.get() === 'B') // contexts are restored 29 | 30 | setTimeout(() => { 31 | console.log(context.get() === 'B') 32 | }, randomTimeout()); 33 | }); 34 | 35 | 36 | context.run("C", async () => { // contexts can be nested. 37 | await wait(randomTimeout()); 38 | 39 | console.log(context.get() === 'C') 40 | 41 | await wait(randomTimeout()); 42 | 43 | console.log(context.get() === 'C') 44 | 45 | setTimeout(() => { 46 | console.log(context.get() === 'C') 47 | }, randomTimeout()); 48 | }); 49 | 50 | }); 51 | 52 | await wait(randomTimeout()); 53 | 54 | console.log(context.get() === 'top'); 55 | 56 | }; 57 | 58 | const main = () => { 59 | context.run('top', example) 60 | } 61 | 62 | Promise.resolve() 63 | .then(() => console.log("START")) 64 | .then(() => main()) 65 | .then(() => console.log("DONE")) 66 | .catch((err) => console.log(err, "ERROR")); 67 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { Polyfill } from "./polyfill/Polyfill"; 2 | 3 | Polyfill.ensureEnabled(); 4 | 5 | export * from "./lib/AsyncContext"; 6 | -------------------------------------------------------------------------------- /src/lib/AsyncContext.ts: -------------------------------------------------------------------------------- 1 | import { AsyncVariable } from "./AsyncVariable"; 2 | import { AsyncSnapshot } from "./AsyncSnapshot"; 3 | 4 | export class AsyncContext { 5 | static Variable = AsyncVariable; 6 | static Snapshot = AsyncSnapshot; 7 | } 8 | -------------------------------------------------------------------------------- /src/lib/AsyncSnapshot.ts: -------------------------------------------------------------------------------- 1 | import { AsyncStack } from "../polyfill/AsyncStack"; 2 | import { AsyncVariable, AsyncStore } from "./AsyncVariable"; 3 | import { runInFork } from "./utils/runInFork"; 4 | 5 | type AnyFunction = (...args: any) => any; 6 | 7 | export class AsyncSnapshot { 8 | private dataByVariable = new Map(); 9 | 10 | private capture() { 11 | let current = AsyncStack.getCurrent(); 12 | while (current) { 13 | const variables = AsyncStore.variableByStack.get(current); 14 | variables?.forEach((variable) => { 15 | const alreadyHasVariable = this.dataByVariable.has(variable); 16 | if (!alreadyHasVariable) { 17 | const value = variable.get(); 18 | this.dataByVariable.set(variable, value); 19 | } 20 | }); 21 | 22 | current = current.origin; 23 | } 24 | } 25 | 26 | constructor() { 27 | this.capture(); 28 | } 29 | 30 | static create() { 31 | const snapshot = new AsyncSnapshot(); 32 | return snapshot; 33 | } 34 | 35 | run(callback: Fn) { 36 | return runInFork(() => { 37 | const current = AsyncStack.getCurrent(); 38 | AsyncStore.stopWalkAt.add(current); 39 | 40 | this.dataByVariable.forEach((data, variable) => { 41 | variable.set(data); 42 | }); 43 | 44 | const result = callback(); 45 | 46 | if (result instanceof Promise) { 47 | return result.then((v) => { 48 | // AsyncStore.stopWalkAt.delete(current); 49 | return v; 50 | }) 51 | .catch((err) => { 52 | // AsyncStore.stopWalkAt.delete(current); 53 | // throw err; 54 | }); 55 | } 56 | return result; 57 | }); 58 | } 59 | 60 | wrap(callback: Fn) { 61 | return (...args) => this.run(() => callback(...args)); 62 | } 63 | 64 | static wrap(callback: Fn) { 65 | return runInFork(() => { 66 | const snapshot = AsyncSnapshot.create(); 67 | return snapshot.wrap(callback); 68 | }); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/lib/AsyncVariable.ts: -------------------------------------------------------------------------------- 1 | import { AsyncStack } from "../polyfill/AsyncStack"; 2 | import { runInFork } from "./utils/runInFork"; 3 | 4 | type AnyFunction = (...args: any) => any; 5 | 6 | type VariableDataBox = { value: Value }; 7 | 8 | export class AsyncStore { 9 | static stopWalkAt = new WeakSet(); 10 | static variableByStack = new WeakMap>(); 11 | static linkVariableWithStack(variable: AsyncVariable, stack: AsyncStack) { 12 | if (!AsyncStore.variableByStack.has(stack)) { 13 | AsyncStore.variableByStack.set(stack, new Set()); 14 | } 15 | 16 | AsyncStore.variableByStack.get(stack).add(variable); 17 | } 18 | } 19 | 20 | export class AsyncVariable { 21 | private data = new WeakMap(); 22 | 23 | private getBox(stack: AsyncStack): VariableDataBox | undefined { 24 | if (!stack) return undefined; 25 | 26 | const currentBox = this.data.get(stack); 27 | if (currentBox) return currentBox; 28 | 29 | const canWalkOrigin = AsyncStore.stopWalkAt.has(stack); 30 | if (canWalkOrigin) return undefined; 31 | 32 | const parentBox = this.getBox(stack.origin); 33 | if (parentBox) this.setBox(stack, parentBox); 34 | return parentBox; 35 | } 36 | 37 | private setBox(stack: AsyncStack, box: { value: Value }) { 38 | AsyncStore.linkVariableWithStack(this, stack); 39 | this.data.set(stack, box); 40 | } 41 | 42 | set(value: Value) { 43 | const current = AsyncStack.getCurrent(); 44 | this.setBox(current, { value }); 45 | } 46 | 47 | get(): Value { 48 | const current = AsyncStack.getCurrent(); 49 | return this.getBox(current)?.value; 50 | } 51 | 52 | run(data: Value, callback: Fn) { 53 | return runInFork(() => { 54 | const current = AsyncStack.getCurrent(); 55 | this.setBox(current, { 56 | value: data, 57 | }); 58 | 59 | return callback(); 60 | }); 61 | } 62 | 63 | wrap(data: Value, callback: Fn): Fn { 64 | // @ts-ignore 65 | return (...args) => this.run(data, () => callback(...args)); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/lib/utils/runInFork.ts: -------------------------------------------------------------------------------- 1 | export const runInFork = (callback: Function) => { 2 | let result; 3 | let error; 4 | 5 | new Promise((resolve) => { 6 | try { 7 | result = callback(); 8 | resolve(result); 9 | } catch (err) { 10 | error = err; 11 | } 12 | }); 13 | 14 | if (error) { 15 | console.log('THROWWW ERROR'); 16 | throw error; 17 | } 18 | 19 | if (result instanceof Promise) { 20 | console.log('--------------------------------'); 21 | console.log('--------------------------------'); 22 | console.log('--------------------------------'); 23 | console.log('--------------------------------'); 24 | console.log('--------------------------------'); 25 | console.log('--------------------------------'); 26 | console.log('result is a promise') 27 | // return result.then((v) => v).catch((err) => { 28 | // console.log('result is a promise error', err) 29 | // throw err; 30 | // }); 31 | } 32 | 33 | return result; 34 | }; 35 | -------------------------------------------------------------------------------- /src/polyfill/AsyncStack.ts: -------------------------------------------------------------------------------- 1 | const GlobalSymbol = Symbol("Global"); 2 | 3 | export class AsyncStack { 4 | // @ts-expect-error 5 | static Global = new AsyncStack(GlobalSymbol); 6 | private static current: AsyncStack = AsyncStack.Global; 7 | 8 | static getCurrent(): AsyncStack { 9 | const current = this.current; 10 | return current; 11 | } 12 | 13 | protected static set(ctx: AsyncStack) { 14 | this.current = ctx; 15 | } 16 | 17 | static NO_DATA = Symbol("NO_DATA"); 18 | 19 | static fork() { 20 | const origin = AsyncStack.getCurrent(); 21 | const fork = new AsyncStack(origin); 22 | fork.start(); 23 | return fork; 24 | } 25 | 26 | origin?: AsyncStack; 27 | 28 | constructor(origin?: AsyncStack) { 29 | // @ts-expect-error 30 | if (origin !== GlobalSymbol) { 31 | this.origin = origin; 32 | } 33 | } 34 | 35 | start() { 36 | AsyncStack.set(this); 37 | } 38 | 39 | // private yielded = false 40 | yield() { 41 | // if (!this.yielded) return; 42 | // this.yielded = true; 43 | AsyncStack.set(this.origin); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/polyfill/Events.ts: -------------------------------------------------------------------------------- 1 | import { AsyncStack } from "./AsyncStack"; 2 | import { runInStack } from "./_lib"; 3 | 4 | const originalAddEventListerner = EventTarget.prototype.addEventListener; 5 | const originalDispatchEvent = EventTarget.prototype.dispatchEvent; 6 | 7 | const stackByEvent = new WeakMap(); 8 | 9 | export const dispatchWithContext: typeof originalDispatchEvent = function ( 10 | event, 11 | ) { 12 | const stack = AsyncStack.getCurrent(); 13 | stackByEvent.set(event, stack); 14 | return originalDispatchEvent.call(this, event); 15 | }; 16 | 17 | export const addEventListenerWithContext: typeof originalAddEventListerner = 18 | function (event, callback, options) { 19 | const wrappedCallback: typeof callback = function (event) { 20 | const stackWhenDispatched = stackByEvent.get(event) || AsyncStack.Global; 21 | return runInStack(stackWhenDispatched, () => { 22 | // @ts-ignore 23 | return callback.call(this, event); 24 | }); 25 | }; 26 | 27 | return originalAddEventListerner.call( 28 | this, 29 | event, 30 | wrappedCallback, 31 | options, 32 | ); 33 | }; 34 | -------------------------------------------------------------------------------- /src/polyfill/Polyfill.ts: -------------------------------------------------------------------------------- 1 | import { OriginalPromise, PromiseWithContext } from "./Promise"; 2 | import { addEventListenerWithContext, dispatchWithContext } from "./Events"; 3 | import { withContext } from "./_lib"; 4 | 5 | const root = 6 | (typeof global !== "undefined" && global) || 7 | (typeof window !== "undefined" && window); 8 | 9 | export class Polyfill { 10 | static originalSetTimeout = setTimeout; 11 | static OriginalPromise = OriginalPromise; 12 | 13 | static enabled = false; 14 | static ensureEnabled() { 15 | if (Polyfill.enabled) return; 16 | Polyfill.enabled = true; 17 | 18 | // Polyfill Promise 19 | root.Promise = PromiseWithContext as any; 20 | 21 | // Polyfill Timers 22 | root.setTimeout = withContext(root.setTimeout); 23 | root.setInterval = withContext(root.setInterval); 24 | root.setImmediate = withContext(root.setImmediate); 25 | 26 | EventTarget.prototype.addEventListener = addEventListenerWithContext; 27 | EventTarget.prototype.dispatchEvent = dispatchWithContext; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/polyfill/Promise.ts: -------------------------------------------------------------------------------- 1 | import { AsyncStack } from "./AsyncStack"; 2 | import { createAsyncResolver, callWithContext } from "./_lib"; 3 | 4 | export const OriginalPromise = Promise; 5 | 6 | export class PromiseWithContext extends OriginalPromise { 7 | constructor(callback: any) { 8 | super((resolve, reject) => { 9 | const fork = AsyncStack.fork(); 10 | const wrapResolve = createAsyncResolver(fork, resolve); 11 | const wrapReject = createAsyncResolver(fork, reject); 12 | callback( 13 | (...args) => { 14 | // console.log('wrapResolve', args) 15 | return wrapResolve(...args) 16 | }, 17 | (...args) => { 18 | // console.log('wrapReject', args) 19 | return wrapReject(...args) 20 | } 21 | ); 22 | fork.yield(); 23 | }) 24 | } 25 | 26 | then(...args: any[]) { 27 | return callWithContext.call(this, super.then, args) 28 | } 29 | 30 | catch(...args: any[]) { 31 | // super.catch(...args); 32 | // console.log('catch') 33 | return callWithContext.call(this, super.catch, args) 34 | } 35 | 36 | finally(...args: any[]) { 37 | return callWithContext.call(this, super.finally, args) 38 | } 39 | } 40 | 41 | // Ensure that all methods of the original Promise 42 | // are available on the new PromiseWithContext 43 | Object.getOwnPropertyNames(Promise).forEach((method) => { 44 | if (typeof Promise[method] === "function") { 45 | PromiseWithContext[method] = 46 | OriginalPromise[method].bind(PromiseWithContext); 47 | } 48 | }); 49 | -------------------------------------------------------------------------------- /src/polyfill/_lib.ts: -------------------------------------------------------------------------------- 1 | import { AsyncStack } from "./AsyncStack"; 2 | 3 | type AnyFunction = (...args: any) => any; 4 | 5 | export const createAsyncResolver = ( 6 | stack: AsyncStack, 7 | callback: AnyFunction, 8 | onlyOnce: boolean = true, 9 | ) => { 10 | let called = false; 11 | 12 | return function (...args: any[]) { 13 | if (onlyOnce && called) return; 14 | called = true; 15 | 16 | stack.yield(); 17 | const fork = AsyncStack.fork() 18 | 19 | try { 20 | 21 | console.log('args', callback, args) 22 | const result = callback.call(this, ...args); 23 | fork.yield() 24 | 25 | return result; 26 | } catch (err) { 27 | fork.yield() 28 | throw err; 29 | } 30 | 31 | 32 | // Note: Is this fork neecessary? All tests are passing without it. 33 | 34 | // console.log('before callback') 35 | // console.log('after callback', result) 36 | }; 37 | }; 38 | 39 | export function callWithContext(originalCallback: AnyFunction, args: any[]) { 40 | const fork = AsyncStack.fork(); 41 | 42 | const patchedArgs = args.map((arg) => { 43 | if (typeof arg === "function") { 44 | return createAsyncResolver(fork, arg); 45 | } 46 | 47 | return arg; 48 | }); 49 | 50 | // console.log('before callWithContext') 51 | const result = originalCallback.call(this, ...patchedArgs); 52 | // console.log('after callWithContext', result) 53 | fork.yield(); 54 | return result; 55 | } 56 | 57 | // This function ensure that the context is passed to the callback 58 | // That is called by the higher order function 59 | export const withContext = ( 60 | originalCallback: Callback, 61 | onlyOnce: boolean = true, 62 | ): Callback => { 63 | if (typeof originalCallback === "undefined") return undefined; 64 | 65 | return function (...args: any[]) { 66 | return callWithContext.call(this, originalCallback, args); 67 | } as any; 68 | }; 69 | 70 | export const runInStack = (stackToUse: AsyncStack, callback: Function) => { 71 | const currentStack = AsyncStack.getCurrent(); 72 | stackToUse.start(); 73 | 74 | try { 75 | const result = callback(); 76 | currentStack.start(); 77 | return result; 78 | } catch (err) { 79 | currentStack.start(); 80 | console.log('runInStack error', err) 81 | throw err; 82 | } 83 | }; 84 | -------------------------------------------------------------------------------- /src/tests/_lib.ts: -------------------------------------------------------------------------------- 1 | import { AsyncStack } from "../polyfill/AsyncStack"; 2 | import { Polyfill } from "../polyfill/Polyfill"; 3 | 4 | export const wait = (timeout: number) => 5 | new Promise((resolve) => Polyfill.originalSetTimeout(resolve, timeout)); 6 | 7 | export const captureAsyncContexts = () => { 8 | const stackTrace: Set = new Set(); 9 | 10 | let lastStack = AsyncStack.getCurrent(); 11 | while (lastStack) { 12 | stackTrace.add(lastStack); 13 | lastStack = lastStack.origin; 14 | } 15 | 16 | return Array.from(stackTrace); 17 | }; 18 | 19 | export const createRecursive = (config: { 20 | deepness: number; 21 | async: boolean; 22 | callback: Function; 23 | }) => { 24 | const recursiveAsync = async (steps: number = config.deepness) => { 25 | if (steps === 1) return config.callback(); 26 | await wait(10); 27 | return await recursiveAsync(steps - 1); 28 | }; 29 | 30 | const recursiveSync = (steps: number = config.deepness) => { 31 | if (steps === 0) return config.callback(); 32 | wait(10); // Just a promise but no `await` 33 | return recursiveSync(steps - 1); 34 | }; 35 | 36 | if (config.async) { 37 | return recursiveAsync; 38 | } 39 | 40 | return recursiveSync; 41 | }; 42 | 43 | export const createAsyncDebugger = (debugId: string) => { 44 | const getDebugStackTrace = (debugId) => { 45 | const stack = captureAsyncContexts().reverse(); 46 | stack.forEach((ctx: any, i) => { 47 | const isAlreadyMarked = ctx.index !== undefined; 48 | if (!isAlreadyMarked) { 49 | ctx.index = i; 50 | ctx.debugId = debugId; 51 | } 52 | }); 53 | 54 | return stack.map((ctx: any, i) => { 55 | return `${ctx.index}/${ctx.debugId}`; 56 | }); 57 | }; 58 | 59 | const root = getDebugStackTrace(debugId); 60 | return { 61 | debug(debugId: string) { 62 | const stackIds = getDebugStackTrace(debugId); 63 | console.log(debugId, stackIds); 64 | }, 65 | }; 66 | }; 67 | -------------------------------------------------------------------------------- /src/tests/async.test.ts: -------------------------------------------------------------------------------- 1 | import { AsyncContext } from ".."; 2 | import { wait, createAsyncDebugger } from "./_lib"; 3 | 4 | const asyncContext = new AsyncContext.Variable(); 5 | 6 | describe("SimpleAsyncContext / Async", () => { 7 | it("async (scenario 1): should know in which context it is", async () => { 8 | const innerCallback = asyncContext.wrap("Inner", () => { 9 | expect(asyncContext.get()).toBe("Inner"); 10 | return "INNER"; 11 | }); 12 | 13 | const total = asyncContext.wrap("Outer", async () => { 14 | expect(asyncContext.get()).toBe("Outer"); 15 | const value = innerCallback(); 16 | expect(asyncContext.get()).toBe("Outer"); 17 | return `OUTER ${value}`; 18 | }); 19 | 20 | await expect(total()).resolves.toBe("OUTER INNER"); 21 | }); 22 | 23 | it("async (scenario 2): should know in which context it is", async () => { 24 | await asyncContext.run("Outer", async () => { 25 | // captureAsyncContexts().forEach((ctx, i) => ctx.index = i); 26 | 27 | // console.log(captureAsyncContexts().map((ctx, i) => ctx.index)); 28 | expect(asyncContext.get()).toBe("Outer"); 29 | // console.log(captureAsyncContexts().map((ctx, i) => ctx.index)); 30 | await wait(30); 31 | 32 | // console.log(captureAsyncContexts().map((ctx, i) => ctx.index)); 33 | expect(asyncContext.get()).toBe("Outer"); 34 | return `OUTER`; 35 | }); 36 | }); 37 | 38 | it("async (scenario 3): should know in which context it is", async () => { 39 | const total = asyncContext.wrap("Outer", async () => { 40 | // console.log(SimpleAsyncContext.getId()); 41 | expect(asyncContext.get()).toBe("Outer"); 42 | await wait(30); 43 | // console.log(SimpleAsyncContext.getId()); 44 | expect(asyncContext.get()).toBe("Outer"); 45 | await wait(30); 46 | // console.log(SimpleAsyncContext.getId()); 47 | expect(asyncContext.get()).toBe("Outer"); 48 | return `OUTER`; 49 | }); 50 | 51 | const value = await total(); 52 | expect(value).toBe("OUTER"); 53 | }); 54 | 55 | it("async (scenario 4): should know in which context it is", async () => { 56 | const innerCallback = async () => { 57 | // console.log('\t -> Inner Content', SimpleAsyncContext.getStackId()); 58 | expect(asyncContext.get()).toBe("Outer"); 59 | return "INNER"; 60 | }; 61 | 62 | const total = asyncContext.wrap("Outer", async () => { 63 | expect(asyncContext.get()).toBe("Outer"); 64 | // console.log('Outer Start') 65 | 66 | // console.log('\t -> Inner Start', SimpleAsyncContext.getStackId()); 67 | const value = await innerCallback(); 68 | 69 | // console.log('\t -> Inner End', SimpleAsyncContext.getStackId()); 70 | 71 | expect(asyncContext.get()).toBe("Outer"); 72 | // console.log('Outer End'); 73 | return `OUTER ${value}`; 74 | }); 75 | 76 | expect(asyncContext.get()).toBe(undefined); 77 | 78 | // console.log('\t -> Before All', SimpleAsyncContext.getStackId()); 79 | const value = await total(); 80 | 81 | expect(asyncContext.get()).toBe(undefined); 82 | 83 | // console.log('\t -> After All', SimpleAsyncContext.getStackId()); 84 | expect(value).toBe("OUTER INNER"); 85 | }); 86 | 87 | it("async (scenario 5): should know in which context it is", async () => { 88 | const innerCallback = async () => { 89 | expect(asyncContext.get()).toBe("Outer"); 90 | return "INNER"; 91 | }; 92 | 93 | const total = asyncContext.wrap("Outer", async () => { 94 | expect(asyncContext.get()).toBe("Outer"); 95 | const value = await innerCallback(); 96 | expect(asyncContext.get()).toBe("Outer"); 97 | const value2 = await innerCallback(); 98 | expect(asyncContext.get()).toBe("Outer"); 99 | const value3 = await innerCallback(); 100 | expect(asyncContext.get()).toBe("Outer"); 101 | return `OUTER ${value} ${value2} ${value3}`; 102 | }); 103 | 104 | expect(asyncContext.get()).toBe(undefined); 105 | await total(); 106 | expect(asyncContext.get()).toBe(undefined); 107 | // expect(value).toBe('OUTER INNER INNER INNER'); 108 | }); 109 | 110 | it("async (scenario 6): should know in which context it is", async () => { 111 | const deepCallback = async () => { 112 | expect(asyncContext.get()).toBe("Outer"); 113 | return "DEEP"; 114 | }; 115 | 116 | const innerCallback = async () => { 117 | expect(asyncContext.get()).toBe("Outer"); 118 | 119 | const value = await deepCallback(); 120 | 121 | expect(asyncContext.get()).toBe("Outer"); 122 | 123 | return `INNER ${value}`; 124 | }; 125 | 126 | const total = asyncContext.wrap("Outer", async () => { 127 | expect(asyncContext.get()).toBe("Outer"); 128 | const value = await innerCallback(); 129 | expect(asyncContext.get()).toBe("Outer"); 130 | const value2 = await innerCallback(); 131 | expect(asyncContext.get()).toBe("Outer"); 132 | const value3 = await innerCallback(); 133 | expect(asyncContext.get()).toBe("Outer"); 134 | return `OUTER ${value} ${value2} ${value3}`; 135 | }); 136 | 137 | expect(asyncContext.get()).toBe(undefined); 138 | await total(); 139 | expect(asyncContext.get()).toBe(undefined); 140 | }); 141 | 142 | it("async (scenario 7): should know in which context it is", async () => { 143 | const deepCallback = async () => { 144 | expect(asyncContext.get()).toBe("Outer"); 145 | 146 | await wait(30); 147 | 148 | expect(asyncContext.get()).toBe("Outer"); 149 | 150 | return "DEEP"; 151 | }; 152 | 153 | const innerCallback = async () => { 154 | expect(asyncContext.get()).toBe("Outer"); 155 | await wait(30); 156 | expect(asyncContext.get()).toBe("Outer"); 157 | 158 | const value = await deepCallback(); 159 | 160 | expect(asyncContext.get()).toBe("Outer"); 161 | await wait(30); 162 | 163 | expect(asyncContext.get()).toBe("Outer"); 164 | 165 | return `INNER ${value}`; 166 | }; 167 | 168 | const total = asyncContext.wrap("Outer", async () => { 169 | expect(asyncContext.get()).toBe("Outer"); 170 | const value = await innerCallback(); 171 | expect(asyncContext.get()).toBe("Outer"); 172 | const value2 = await innerCallback(); 173 | expect(asyncContext.get()).toBe("Outer"); 174 | const value3 = await innerCallback(); 175 | expect(asyncContext.get()).toBe("Outer"); 176 | return `OUTER ${value} ${value2} ${value3}`; 177 | }); 178 | 179 | expect(asyncContext.get()).toBe(undefined); 180 | await total(); 181 | expect(asyncContext.get()).toBe(undefined); 182 | }); 183 | 184 | it("async (scenario 8): should know in which context it is", async () => { 185 | const deepCallback = async () => { 186 | expect(asyncContext.get()).toBe("Inner"); 187 | await wait(30); 188 | expect(asyncContext.get()).toBe("Inner"); 189 | return "DEEP"; 190 | }; 191 | 192 | const innerCallback = asyncContext.wrap("Inner", async () => { 193 | expect(asyncContext.get()).toBe("Inner"); 194 | await wait(30); 195 | expect(asyncContext.get()).toBe("Inner"); 196 | const value = await deepCallback(); 197 | expect(asyncContext.get()).toBe("Inner"); 198 | await wait(30); 199 | expect(asyncContext.get()).toBe("Inner"); 200 | return `INNER ${value}`; 201 | }); 202 | 203 | const total = asyncContext.wrap("Outer", async () => { 204 | expect(asyncContext.get()).toBe("Outer"); 205 | const value = await innerCallback(); 206 | expect(asyncContext.get()).toBe("Outer"); 207 | const value2 = await innerCallback(); 208 | expect(asyncContext.get()).toBe("Outer"); 209 | const value3 = await innerCallback(); 210 | expect(asyncContext.get()).toBe("Outer"); 211 | return `OUTER ${value} ${value2} ${value3}`; 212 | }); 213 | 214 | expect(asyncContext.get()).toBe(undefined); 215 | await total(); 216 | expect(asyncContext.get()).toBe(undefined); 217 | }); 218 | 219 | it("async (scenario 9): should know in which context it is", async () => { 220 | // const debugAsync = createAsyncDebugger('global'); 221 | const track1 = asyncContext.wrap("track1", async () => { 222 | // debugAsync.debug('track1.1'); 223 | 224 | expect(asyncContext.get()).toBe("track1"); 225 | await wait(30); 226 | 227 | // debugAsync.debug('track1.2'); 228 | expect(asyncContext.get()).toBe("track1"); 229 | }); 230 | 231 | const track2 = asyncContext.wrap("track2", async () => { 232 | // debugAsync.debug('track2.1'); 233 | expect(asyncContext.get()).toBe("track2"); 234 | await wait(30); 235 | 236 | // debugAsync.debug('track2.2'); 237 | expect(asyncContext.get()).toBe("track2"); 238 | }); 239 | 240 | expect(asyncContext.get()).toBe(undefined); 241 | 242 | track1(); 243 | track2(); 244 | 245 | await wait(100); 246 | expect(asyncContext.get()).toBe(undefined); 247 | }); 248 | 249 | it("async (scenario 9/bis): should know in which context it is", async () => { 250 | const track1 = asyncContext.wrap("track1", async () => { 251 | expect(asyncContext.get()).toBe("track1"); 252 | await wait(30); 253 | expect(asyncContext.get()).toBe("track1"); 254 | }); 255 | 256 | const track2 = asyncContext.wrap("track2", async () => { 257 | expect(asyncContext.get()).toBe("track2"); 258 | await wait(30); 259 | expect(asyncContext.get()).toBe("track2"); 260 | }); 261 | 262 | expect(asyncContext.get()).toBe(undefined); 263 | 264 | track1(); 265 | await wait(30); 266 | track2(); 267 | 268 | await wait(100); 269 | expect(asyncContext.get()).toBe(undefined); 270 | }); 271 | 272 | it("async (scenario 9/bis/bis): should know in which context it is", async () => { 273 | const track1 = asyncContext.wrap("track1", async () => { 274 | expect(asyncContext.get()).toBe("track1"); 275 | await wait(30); 276 | expect(asyncContext.get()).toBe("track1"); 277 | }); 278 | 279 | const track2 = asyncContext.wrap("track2", async () => { 280 | expect(asyncContext.get()).toBe("track2"); 281 | await wait(30); 282 | expect(asyncContext.get()).toBe("track2"); 283 | }); 284 | 285 | expect(asyncContext.get()).toBe(undefined); 286 | 287 | track1().then(() => { 288 | expect(asyncContext.get()).toBe(undefined); 289 | }); 290 | 291 | await wait(30); 292 | track2().then(() => { 293 | expect(asyncContext.get()).toBe(undefined); 294 | }); 295 | 296 | await wait(100); 297 | expect(asyncContext.get()).toBe(undefined); 298 | }); 299 | 300 | it("async (scenario 9/bis/bis/bis): should know in which context it is", async () => { 301 | const track1 = asyncContext.wrap("track1", async () => { 302 | expect(asyncContext.get()).toBe("track1"); 303 | await wait(30); 304 | expect(asyncContext.get()).toBe("track1"); 305 | }); 306 | 307 | const track2 = asyncContext.wrap("track2", async () => { 308 | expect(asyncContext.get()).toBe("track2"); 309 | await wait(30); 310 | expect(asyncContext.get()).toBe("track2"); 311 | }); 312 | 313 | expect(asyncContext.get()).toBe(undefined); 314 | 315 | let trackedAsyncData: any = {}; 316 | asyncContext.run('Random Wrap', async () => { 317 | track1().then(() => { 318 | trackedAsyncData = asyncContext.get(); 319 | }); 320 | }); 321 | 322 | // await wait(30) 323 | 324 | let trackedAsyncData2: any = {}; 325 | track2().then(() => { 326 | trackedAsyncData2 = asyncContext.get(); 327 | }); 328 | 329 | await wait(100); 330 | 331 | expect(trackedAsyncData).toBe("Random Wrap"); 332 | expect(trackedAsyncData2).toBe(undefined); 333 | expect(asyncContext.get()).toBe(undefined); 334 | }); 335 | 336 | it("async (scenario 10): should know in which context it is", async () => { 337 | const innerCallback = asyncContext.wrap("Inner", async () => { 338 | expect(asyncContext.get()).toBe("Inner"); 339 | await wait(30); 340 | expect(asyncContext.get()).toBe("Inner"); 341 | await wait(30); 342 | expect(asyncContext.get()).toBe("Inner"); 343 | return `INNER`; 344 | }); 345 | 346 | const total = asyncContext.wrap("Outer", async () => { 347 | expect(asyncContext.get()).toBe("Outer"); 348 | const value = await innerCallback(); 349 | expect(asyncContext.get()).toBe("Outer"); 350 | const value2 = await innerCallback(); 351 | expect(asyncContext.get()).toBe("Outer"); 352 | const value3 = await innerCallback(); 353 | expect(asyncContext.get()).toBe("Outer"); 354 | return `OUTER ${value}`; 355 | }); 356 | 357 | expect(asyncContext.get()).toBe(undefined); 358 | const results = await Promise.all([total(), total()]); 359 | 360 | expect(asyncContext.get()).toBe(undefined); 361 | expect(results).toEqual(["OUTER INNER", "OUTER INNER"]); 362 | }); 363 | 364 | it("async (scenario 11): should know in which context it is", async () => { 365 | const innerCallback = asyncContext.wrap("Inner", async () => { 366 | expect(asyncContext.get()).toBe("Inner"); 367 | // console.log(SimpleAsyncContext.getStackId()) 368 | await wait(30); 369 | // console.log(SimpleAsyncContext.getStackId()) 370 | expect(asyncContext.get()).toBe("Inner"); 371 | await wait(30); 372 | // console.log(SimpleAsyncContext.getStackId()) 373 | expect(asyncContext.get()).toBe("Inner"); 374 | return `INNER`; 375 | }); 376 | 377 | const inner2Callback = asyncContext.wrap("Inner2", async () => { 378 | // console.log(SimpleAsyncContext.getStackId()) 379 | expect(asyncContext.get()).toBe("Inner2"); 380 | await wait(30); 381 | // console.log(SimpleAsyncContext.getStackId()) 382 | expect(asyncContext.get()).toBe("Inner2"); 383 | // console.log(SimpleAsyncContext.getStackId()) 384 | await wait(30); 385 | expect(asyncContext.get()).toBe("Inner2"); 386 | return `INNER2`; 387 | }); 388 | 389 | const total = asyncContext.wrap("Outer", async () => { 390 | expect(asyncContext.get()).toBe("Outer"); 391 | const value = await innerCallback(); 392 | expect(asyncContext.get()).toBe("Outer"); 393 | const value2 = await inner2Callback(); 394 | expect(asyncContext.get()).toBe("Outer"); 395 | const value3 = await innerCallback(); 396 | expect(asyncContext.get()).toBe("Outer"); 397 | return `OUTER ${value}`; 398 | }); 399 | 400 | const total2 = asyncContext.wrap("Outer2", async () => { 401 | expect(asyncContext.get()).toBe("Outer2"); 402 | const value = await innerCallback(); 403 | expect(asyncContext.get()).toBe("Outer2"); 404 | const value2 = await inner2Callback(); 405 | expect(asyncContext.get()).toBe("Outer2"); 406 | const value3 = await innerCallback(); 407 | expect(asyncContext.get()).toBe("Outer2"); 408 | return `OUTER2 ${value2}`; 409 | }); 410 | 411 | expect(asyncContext.get()).toBe(undefined); 412 | const results = await Promise.all([total(), total2(), total(), total2()]); 413 | 414 | expect(asyncContext.get()).toBe(undefined); 415 | expect(results).toEqual([ 416 | "OUTER INNER", 417 | "OUTER2 INNER2", 418 | "OUTER INNER", 419 | "OUTER2 INNER2", 420 | ]); 421 | }); 422 | 423 | it("async (scenario 12): should know in which context it is", async () => { 424 | const overflowInner = asyncContext.wrap("Overflow", async () => { 425 | expect(asyncContext.get()).toBe("Overflow"); 426 | await wait(25); 427 | expect(asyncContext.get()).toBe("Overflow"); 428 | }); 429 | 430 | const innerCallback = asyncContext.wrap("Inner", async () => { 431 | expect(asyncContext.get()).toBe("Inner"); 432 | overflowInner(); 433 | // console.log(SimpleAsyncContext.getStackId()) 434 | await wait(30); 435 | overflowInner(); 436 | expect(asyncContext.get()).toBe("Inner"); 437 | await wait(30); 438 | // console.log(SimpleAsyncContext.getStackId()) 439 | expect(asyncContext.get()).toBe("Inner"); 440 | return `INNER`; 441 | }); 442 | 443 | const total = asyncContext.wrap("Outer", async () => { 444 | expect(asyncContext.get()).toBe("Outer"); 445 | const value = innerCallback(); 446 | expect(asyncContext.get()).toBe("Outer"); 447 | const value2 = await overflowInner(); 448 | expect(asyncContext.get()).toBe("Outer"); 449 | const value3 = await innerCallback(); 450 | expect(asyncContext.get()).toBe("Outer"); 451 | }); 452 | 453 | await total(); 454 | await wait(80); 455 | }); 456 | 457 | 458 | it("async (scenario 13): should know in which context it is", async () => { 459 | const overflowInner = asyncContext.wrap("Overflow", async () => { 460 | setTimeout(async () => { 461 | expect(asyncContext.get()).toBe("Overflow"); 462 | await wait(25); 463 | expect(asyncContext.get()).toBe("Overflow"); 464 | }, 100) 465 | }); 466 | 467 | const innerCallback = asyncContext.wrap("Inner", async () => { 468 | expect(asyncContext.get()).toBe("Inner"); 469 | overflowInner(); 470 | // console.log(SimpleAsyncContext.getStackId()) 471 | await wait(30); 472 | overflowInner(); 473 | expect(asyncContext.get()).toBe("Inner"); 474 | await wait(30); 475 | // console.log(SimpleAsyncContext.getStackId()) 476 | expect(asyncContext.get()).toBe("Inner"); 477 | }); 478 | 479 | await asyncContext.run("Outer", async () => { 480 | expect(asyncContext.get()).toBe("Outer"); 481 | innerCallback(); 482 | expect(asyncContext.get()).toBe("Outer"); 483 | await overflowInner(); 484 | expect(asyncContext.get()).toBe("Outer"); 485 | await innerCallback(); 486 | expect(asyncContext.get()).toBe("Outer"); 487 | }); 488 | }); 489 | 490 | }); 491 | -------------------------------------------------------------------------------- /src/tests/error.test.ts: -------------------------------------------------------------------------------- 1 | import { AsyncContext } from "../"; 2 | import { wait, createAsyncDebugger } from "./_lib"; 3 | 4 | const asyncContext = new AsyncContext.Variable(); 5 | 6 | describe("SimpleAsyncContext / Async", () => { 7 | 8 | it("error (scenario 1): should know in which context it is", async () => { 9 | const total = asyncContext.wrap("Outer", async () => { 10 | expect(asyncContext.get()).toBe("Outer"); 11 | await wait(30); 12 | throw new Error("Overflow") 13 | }); 14 | 15 | const result = total(); 16 | console.log('result', result) 17 | await expect(total()).rejects.toThrow("Overflow"); 18 | 19 | // try { 20 | 21 | // await total(); 22 | // await wait(80); 23 | // } catch (error) { 24 | 25 | // console.log('--------------------------------'); 26 | // console.log('asyncContext', asyncContext.get()); 27 | // console.log('--------------------------------'); 28 | // console.log('erroruuuuu', error) 29 | // // expect(error).toBeInstanceOf(Error); 30 | // // expect(error.message).toBe("Overflow"); 31 | // } 32 | 33 | // expect(asyncContext.get()).toBe(undefined); 34 | 35 | // console.log('after') 36 | }); 37 | 38 | 39 | }); 40 | -------------------------------------------------------------------------------- /src/tests/misc.test.ts: -------------------------------------------------------------------------------- 1 | import { AsyncContext } from ".."; 2 | import { Polyfill } from "../polyfill/Polyfill"; 3 | import { captureAsyncContexts, createRecursive, wait } from "./_lib"; 4 | import { withContext } from "../polyfill/_lib"; 5 | import { AsyncVariable } from "../lib/AsyncVariable"; 6 | import { AsyncStack } from "../polyfill/AsyncStack"; 7 | 8 | const asyncContext = new AsyncContext.Variable(); 9 | 10 | describe("Misc", () => { 11 | describe("Misc / Data", () => { 12 | const values = [null, undefined, 0, false]; 13 | values.forEach((value) => { 14 | it(`should accept ${value} as data`, async () => { 15 | await testContextData(value); 16 | }); 17 | }); 18 | 19 | it(`should point to the original data`, async () => { 20 | await testContextData(Symbol("test")); 21 | }); 22 | }); 23 | 24 | describe("EventTarget", () => { 25 | const eventTarget = new EventTarget(); 26 | const context = new AsyncContext.Variable(); 27 | const context2 = new AsyncContext.Variable(); 28 | 29 | const eventName = "test-event"; 30 | const contextData = "test-context-data"; 31 | const eventListenerSpy = jest.fn(); 32 | 33 | const eventListener = (event) => { 34 | const value = context.get(); 35 | eventListenerSpy(value); 36 | }; 37 | 38 | context.run("noise", () => { 39 | context2.run("noise", () => { 40 | eventTarget.addEventListener(eventName, eventListener); 41 | }); 42 | }); 43 | 44 | it("should be able to track context within an event listener", async () => { 45 | const prevStack = AsyncStack.getCurrent(); 46 | 47 | await context.run(contextData, async () => { 48 | eventTarget.dispatchEvent(new Event(eventName)); 49 | eventTarget.dispatchEvent(new Event(eventName)); 50 | eventTarget.dispatchEvent(new Event(eventName)); 51 | }); 52 | 53 | const afterStack = AsyncStack.getCurrent(); 54 | 55 | expect(prevStack).toBe(afterStack); 56 | await wait(100); 57 | 58 | expect(eventListenerSpy).toHaveBeenCalledTimes(3); 59 | eventListenerSpy.mock.calls.forEach(([value]) => { 60 | expect(value).toBe(contextData); 61 | }); 62 | 63 | eventListenerSpy.mockClear(); 64 | }); 65 | 66 | it("should be able to removeEventListener", async () => { 67 | eventTarget.removeEventListener(eventName, eventListener); 68 | 69 | expect(eventListenerSpy).toHaveBeenCalledTimes(0); 70 | eventListenerSpy.mockClear(); 71 | }); 72 | }); 73 | 74 | describe("withContext", () => { 75 | it("should propagate `this`", () => { 76 | const wrapped = withContext(function () { 77 | expect(this).toBe("test"); 78 | }); 79 | 80 | wrapped.call("test"); 81 | }); 82 | 83 | it('return a Promises', async () => { 84 | const createPromise = async () => { 85 | await wait(1000); 86 | return true; 87 | }; 88 | 89 | const promise = createPromise(); 90 | 91 | expect(promise instanceof Promise).toBe(true); 92 | await expect(promise).resolves.toBe(true) 93 | }) 94 | }); 95 | 96 | it("traces back to root context in sync", () => { 97 | const DEEPNESS = 10; 98 | const currentStack = captureAsyncContexts(); 99 | const unfoldAsyncStack = createRecursive({ 100 | async: false, 101 | deepness: DEEPNESS, 102 | callback: captureAsyncContexts, 103 | }); 104 | 105 | const stackTrace = unfoldAsyncStack(); 106 | expect(stackTrace.length).toEqual(currentStack.length); 107 | 108 | const topmostStask = stackTrace[stackTrace.length - 1]; 109 | expect(topmostStask).toEqual(AsyncStack.Global); 110 | }); 111 | 112 | it("traces back to root context in async", async () => { 113 | const DEEPNESS = 10; 114 | const currentStack = captureAsyncContexts(); 115 | const unfoldAsyncStack = createRecursive({ 116 | async: true, 117 | deepness: DEEPNESS, 118 | callback: captureAsyncContexts, 119 | }); 120 | 121 | const stackTrace = await unfoldAsyncStack(); 122 | expect(stackTrace.length).toEqual(DEEPNESS + currentStack.length); 123 | 124 | const topmostStask = stackTrace[stackTrace.length - 1]; 125 | expect(topmostStask).toEqual(AsyncStack.Global); 126 | }); 127 | 128 | it("should cache variable when traversing deep stack", async () => { 129 | const context = new AsyncContext.Variable(); 130 | const spy = jest.spyOn(AsyncVariable.prototype, "getBox" as any); 131 | 132 | const calls = []; 133 | const recursiveContextCallback = async ( 134 | remainaingRecusrsiveSteps: number, 135 | ) => { 136 | if (remainaingRecusrsiveSteps === 0) { 137 | context.get(); 138 | calls.push(spy.mock.calls.length); 139 | spy.mockClear(); 140 | 141 | context.get(); 142 | calls.push(spy.mock.calls.length); 143 | spy.mockClear(); 144 | return; 145 | } 146 | 147 | await recursiveContextCallback(remainaingRecusrsiveSteps - 1); 148 | }; 149 | 150 | await context.run("A", async () => { 151 | await recursiveContextCallback(10); 152 | }); 153 | 154 | expect(calls[0]).toBeGreaterThanOrEqual(10); 155 | expect(calls[1]).toBe(1); 156 | expect(calls).toHaveLength(2); 157 | // expect(spy).toHaveBeenCalledTimes(1); 158 | }); 159 | 160 | it("should keep a reference to the original setTimeout", () => { 161 | expect(Polyfill.originalSetTimeout === setTimeout).toBe(false); 162 | }); 163 | 164 | it("example from readme should work", async () => { 165 | const context = new AsyncContext.Variable(); 166 | 167 | const wait = (timeout: number) => 168 | new Promise((r) => setTimeout(r, timeout)); 169 | const randomTimeout = () => Math.random() * 200; 170 | 171 | async function main() { 172 | expect(context.get()).toBe("top"); 173 | 174 | await wait(randomTimeout()); 175 | 176 | context.run("A", () => { 177 | expect(context.get()).toBe("A"); 178 | 179 | setTimeout(() => { 180 | expect(context.get()).toBe("A"); 181 | }, randomTimeout()); 182 | 183 | context.run("B", async () => { 184 | // contexts can be nested. 185 | await wait(randomTimeout()); 186 | 187 | expect(context.get()).toBe("B"); 188 | 189 | expect(context.get()).toBe("B"); // contexts are restored ) 190 | 191 | setTimeout(() => { 192 | expect(context.get()).toBe("B"); 193 | }, randomTimeout()); 194 | }); 195 | 196 | context.run("C", async () => { 197 | // contexts can be nested. 198 | await wait(randomTimeout()); 199 | 200 | expect(context.get()).toBe("C"); 201 | 202 | await wait(randomTimeout()); 203 | 204 | expect(context.get()).toBe("C"); 205 | 206 | setTimeout(() => { 207 | expect(context.get()).toBe("C"); 208 | }, randomTimeout()); 209 | }); 210 | }); 211 | 212 | await wait(randomTimeout()); 213 | 214 | expect(context.get()).toBe("top"); 215 | } 216 | 217 | await context.run("top", main); 218 | 219 | await wait(1000); 220 | }); 221 | }); 222 | 223 | const testContextData = async (contextData: any) => { 224 | const deepInnerWrapperCallback = async () => { 225 | expect(asyncContext.get()).toEqual(contextData); 226 | await wait(100); 227 | expect(asyncContext.get()).toEqual(contextData); 228 | }; 229 | 230 | const innerCallback = asyncContext.wrap(contextData, async () => { 231 | await deepInnerWrapperCallback(); 232 | await wait(100); 233 | await deepInnerWrapperCallback(); 234 | }); 235 | 236 | const total = asyncContext.wrap("Outer", async () => { 237 | await innerCallback(); 238 | }); 239 | 240 | const innerCallback2 = asyncContext.wrap(contextData, async () => { 241 | await deepInnerWrapperCallback(); 242 | await wait(100); 243 | await deepInnerWrapperCallback(); 244 | }); 245 | 246 | const total2 = asyncContext.wrap("Outer2", async () => { 247 | await innerCallback2(); 248 | }); 249 | 250 | await Promise.all([total(), total2()]); 251 | }; 252 | -------------------------------------------------------------------------------- /src/tests/snapshot.test.ts: -------------------------------------------------------------------------------- 1 | import { AsyncContext } from ".."; 2 | import { AsyncSnapshot } from "../lib/AsyncSnapshot"; 3 | import { wait } from "./_lib"; 4 | 5 | describe("AsyncContext / Snapshot", () => { 6 | it("can runs callback", () => { 7 | const snapshot = AsyncContext.Snapshot.create(); 8 | const result = snapshot.run(() => "Done"); 9 | expect(result).toBe("Done"); 10 | }); 11 | 12 | it("can wraps callback", () => { 13 | const snapshot = AsyncContext.Snapshot.create(); 14 | const result = snapshot.wrap(() => "Done"); 15 | expect(result()).toBe("Done"); 16 | }); 17 | 18 | it("snapshot (scenario 1): should know in which context it is", async () => { 19 | const asyncContext = new AsyncContext.Variable(); 20 | let snapshot: AsyncSnapshot = null; 21 | 22 | const total = asyncContext.wrap("Outer", async () => { 23 | snapshot = AsyncContext.Snapshot.create(); 24 | }); 25 | 26 | expect(asyncContext.get()).toBe(undefined); 27 | 28 | await total(); 29 | 30 | expect(asyncContext.get()).toBe(undefined); 31 | 32 | snapshot.run(() => { 33 | expect(asyncContext.get()).toBe("Outer"); 34 | }); 35 | 36 | expect(asyncContext.get()).toBe(undefined); 37 | }); 38 | 39 | it("snapshot (scenario 2): should know in which context it is", async () => { 40 | const asyncContext = new AsyncContext.Variable(); 41 | let snapshot: AsyncSnapshot = null; 42 | 43 | const total = asyncContext.wrap("Outer", async () => { 44 | await wait(100); 45 | snapshot = AsyncContext.Snapshot.create(); 46 | }); 47 | 48 | expect(asyncContext.get()).toBe(undefined); 49 | 50 | await total(); 51 | 52 | expect(asyncContext.get()).toBe(undefined); 53 | 54 | snapshot.run(async () => { 55 | await wait(100); 56 | expect(asyncContext.get()).toBe("Outer"); 57 | }); 58 | 59 | expect(asyncContext.get()).toBe(undefined); 60 | }); 61 | 62 | it("snapshot (scenario 3): should know in which context it is", async () => { 63 | let snapshot: AsyncSnapshot = null; 64 | const asyncContext = new AsyncContext.Variable(); 65 | const asyncContext2 = new AsyncContext.Variable(); 66 | 67 | const innerCallback = asyncContext.wrap("Inner", async () => { 68 | return asyncContext2.run("Inner 2", async () => { 69 | await wait(100); 70 | snapshot = AsyncContext.Snapshot.create(); 71 | }); 72 | }); 73 | 74 | const total = asyncContext.wrap("Outer", async () => { 75 | await innerCallback(); 76 | }); 77 | 78 | expect(asyncContext.get()).toBe(undefined); 79 | expect(asyncContext2.get()).toBe(undefined); 80 | 81 | await total(); 82 | 83 | expect(asyncContext.get()).toBe(undefined); 84 | expect(asyncContext2.get()).toBe(undefined); 85 | 86 | snapshot.run(() => { 87 | expect(asyncContext.get()).toBe("Inner"); 88 | expect(asyncContext2.get()).toBe("Inner 2"); 89 | }); 90 | 91 | expect(asyncContext.get()).toBe(undefined); 92 | expect(asyncContext2.get()).toBe(undefined); 93 | }); 94 | 95 | it("snapshot (scenario 4): should know in which context it is", async () => { 96 | const asyncContext = new AsyncContext.Variable(); 97 | const asyncContext2 = new AsyncContext.Variable(); 98 | 99 | let snapshot; 100 | await asyncContext.run("Outer", () => { 101 | return asyncContext.run("Middle", async () => { 102 | asyncContext.get(); 103 | await wait(100); 104 | return asyncContext2.run("Inner", async () => { 105 | asyncContext.get(); 106 | await wait(100); 107 | snapshot = AsyncContext.Snapshot.create(); 108 | }); 109 | }); 110 | }); 111 | 112 | snapshot.run(() => { 113 | expect(asyncContext.get()).toBe("Middle"); 114 | expect(asyncContext2.get()).toBe("Inner"); 115 | }); 116 | 117 | await wait(1000); 118 | }); 119 | 120 | it("snapshot (scenario 5): should know in which context it is", async () => { 121 | const asyncContext = new AsyncContext.Variable(); 122 | const asyncContext2 = new AsyncContext.Variable(); 123 | 124 | let snapshot: AsyncSnapshot = null; 125 | 126 | const innerCallback = asyncContext.wrap("Inner", async () => { 127 | return asyncContext2.run("Inner 2", async () => { 128 | expect(asyncContext.get()).toBe("Inner"); 129 | asyncContext2.get(); 130 | await wait(100); 131 | snapshot = AsyncContext.Snapshot.create(); 132 | expect(asyncContext.get()).toBe("Inner"); 133 | }); 134 | }); 135 | 136 | const total = asyncContext.wrap("Outer", async () => { 137 | expect(asyncContext.get()).toBe("Outer"); 138 | await innerCallback(); 139 | expect(asyncContext.get()).toBe("Outer"); 140 | }); 141 | 142 | await total(); 143 | 144 | expect(asyncContext.get()).toBe(undefined); 145 | expect(asyncContext2.get()).toBe(undefined); 146 | 147 | snapshot.run(async () => { 148 | expect(asyncContext.get()).toBe("Inner"); 149 | expect(asyncContext2.get()).toBe("Inner 2"); 150 | await wait(100); 151 | 152 | asyncContext.run("Outer:WithinSnapshot", async () => { 153 | await wait(100); 154 | expect(asyncContext.get()).toBe("Outer:WithinSnapshot"); 155 | expect(asyncContext2.get()).toBe("Inner 2"); 156 | }); 157 | }); 158 | 159 | expect(asyncContext.get()).toBe(undefined); 160 | expect(asyncContext2.get()).toBe(undefined); 161 | 162 | await wait(1000); 163 | }); 164 | 165 | it("scenario 6: should know in which context it is", () => { 166 | type Value = { id: number }; 167 | const a = new AsyncContext.Variable(); 168 | const b = new AsyncContext.Variable(); 169 | const first = { id: 1 }; 170 | const second = { id: 2 }; 171 | 172 | const snapshotCallback = a.run(first, () => { 173 | const snapshot = new AsyncContext.Snapshot() 174 | return snapshot.wrap(() => { 175 | expect(a.get()).toBe(first); 176 | expect(b.get()).toBe(undefined); 177 | }); 178 | }); 179 | 180 | b.run(second, () => { 181 | snapshotCallback(); 182 | }); 183 | 184 | expect(a.get()).toBe(undefined); 185 | expect(b.get()).toBe(undefined); 186 | }); 187 | }); 188 | -------------------------------------------------------------------------------- /src/tests/sync.test.ts: -------------------------------------------------------------------------------- 1 | import { AsyncContext } from ".."; 2 | 3 | const asyncContext = new AsyncContext.Variable(); 4 | 5 | describe("SimpleAsyncContext / Sync", () => { 6 | it("runs the callback", () => { 7 | const spy = jest.fn(); 8 | 9 | asyncContext.run("Silbling", () => { 10 | spy(); 11 | }); 12 | 13 | expect(spy).toHaveBeenCalledTimes(1); 14 | spy.mockClear(); 15 | }); 16 | 17 | it("sync (scenario 1): should know in which context it is", () => { 18 | const spy = jest.fn(); 19 | const silblingCallback = asyncContext.wrap("Silbling", () => { 20 | spy(); 21 | expect(asyncContext.get()).toBe("Silbling"); 22 | }); 23 | 24 | expect(asyncContext.get()).toBe(undefined); 25 | 26 | silblingCallback(); 27 | expect(spy).toHaveBeenCalledTimes(1); 28 | spy.mockClear(); 29 | 30 | expect(asyncContext.get()).toBe(undefined); 31 | 32 | silblingCallback(); 33 | expect(spy).toHaveBeenCalledTimes(1); 34 | spy.mockClear(); 35 | 36 | expect(asyncContext.get()).toBe(undefined); 37 | }); 38 | 39 | it("sync (scenario 2): should know in which context it is", async () => { 40 | const deepInnerCallback = asyncContext.wrap("DeepInner", () => { 41 | expect(asyncContext.get()).toBe("DeepInner"); 42 | return "DEEP"; 43 | }); 44 | 45 | const deepInnerWrapperCallback = async () => { 46 | // <-- this is an async function 47 | expect(asyncContext.get()).toBe("Inner"); 48 | const value = deepInnerCallback(); 49 | expect(asyncContext.get()).toBe("Inner"); 50 | return value; 51 | }; 52 | 53 | const innerCallback = asyncContext.wrap("Inner", () => { 54 | expect(asyncContext.get()).toBe("Inner"); 55 | deepInnerWrapperCallback(); 56 | expect(asyncContext.get()).toBe("Inner"); 57 | return "INNER"; 58 | }); 59 | 60 | const total = asyncContext.wrap("Outer", () => { 61 | expect(asyncContext.get()).toBe("Outer"); 62 | const inner = innerCallback(); 63 | expect(asyncContext.get()).toBe("Outer"); 64 | return "OUTER" + " " + inner; 65 | }); 66 | 67 | expect(total()).toBe("OUTER INNER"); 68 | }); 69 | 70 | it("sync (scenario 3): should know in which context it is", () => { 71 | const deepInnerWrapperCallback = () => { 72 | expect(asyncContext.get()).toBe("Inner"); 73 | const value = deepInnerCallback(); 74 | expect(asyncContext.get()).toBe("Inner"); 75 | return value; 76 | }; 77 | const deepInnerCallback = asyncContext.wrap("DeepInner", () => { 78 | expect(asyncContext.get()).toBe("DeepInner"); 79 | return "DEEP"; 80 | }); 81 | 82 | const innerCallback = asyncContext.wrap("Inner", () => { 83 | expect(asyncContext.get()).toBe("Inner"); 84 | const deep = deepInnerWrapperCallback(); 85 | expect(asyncContext.get()).toBe("Inner"); 86 | return "INNER" + " " + deep; 87 | }); 88 | 89 | const silblingCallback = asyncContext.wrap("Silbling", () => { 90 | expect(asyncContext.get()).toBe("Silbling"); 91 | return "SILBLING"; 92 | }); 93 | 94 | const total = asyncContext.wrap("Outer", () => { 95 | expect(asyncContext.get()).toBe("Outer"); 96 | const inner = innerCallback(); 97 | expect(asyncContext.get()).toBe("Outer"); 98 | return "OUTER" + " " + inner; 99 | }); 100 | 101 | expect(asyncContext.get()).toBe(undefined); 102 | expect(silblingCallback()).toBe("SILBLING"); 103 | expect(asyncContext.get()).toBe(undefined); 104 | expect(total()).toBe("OUTER INNER DEEP"); 105 | expect(asyncContext.get()).toBe(undefined); 106 | expect(silblingCallback()).toBe("SILBLING"); 107 | expect(asyncContext.get()).toBe(undefined); 108 | }); 109 | }); 110 | -------------------------------------------------------------------------------- /src/tests/tc39.test.ts: -------------------------------------------------------------------------------- 1 | import { AsyncContext } from "../"; 2 | 3 | type Value = { id: number }; 4 | 5 | const assert = { 6 | equal: (actual: any, expected: any) => { 7 | expect(actual).toEqual(expected); 8 | }, 9 | }; 10 | 11 | describe("TC39 - Async Context Test", () => { 12 | describe("sync", () => { 13 | describe("run and get", () => { 14 | test("has initial undefined state", () => { 15 | const ctx = new AsyncContext.Variable(); 16 | 17 | const actual = ctx.get(); 18 | 19 | assert.equal(actual, undefined); 20 | }); 21 | 22 | test("return value", () => { 23 | const ctx = new AsyncContext.Variable(); 24 | const expected = { id: 1 }; 25 | 26 | const actual = ctx.run({ id: 2 }, () => expected); 27 | 28 | assert.equal(actual, expected); 29 | }); 30 | 31 | test("get returns current context value", () => { 32 | const ctx = new AsyncContext.Variable(); 33 | const expected = { id: 1 }; 34 | 35 | ctx.run(expected, () => { 36 | assert.equal(ctx.get(), expected); 37 | }); 38 | }); 39 | 40 | test("get within nesting contexts", () => { 41 | const ctx = new AsyncContext.Variable(); 42 | const first = { id: 1 }; 43 | const second = { id: 2 }; 44 | 45 | ctx.run(first, () => { 46 | assert.equal(ctx.get(), first); 47 | ctx.run(second, () => { 48 | assert.equal(ctx.get(), second); 49 | }); 50 | assert.equal(ctx.get(), first); 51 | }); 52 | assert.equal(ctx.get(), undefined); 53 | }); 54 | 55 | test("get within nesting different contexts", () => { 56 | const a = new AsyncContext.Variable(); 57 | const b = new AsyncContext.Variable(); 58 | const first = { id: 1 }; 59 | const second = { id: 2 }; 60 | 61 | a.run(first, () => { 62 | assert.equal(a.get(), first); 63 | assert.equal(b.get(), undefined); 64 | b.run(second, () => { 65 | assert.equal(a.get(), first); 66 | assert.equal(b.get(), second); 67 | }); 68 | assert.equal(a.get(), first); 69 | assert.equal(b.get(), undefined); 70 | }); 71 | assert.equal(a.get(), undefined); 72 | assert.equal(b.get(), undefined); 73 | }); 74 | }); 75 | 76 | describe("wrap", () => { 77 | test("stores initial undefined state", () => { 78 | const ctx = new AsyncContext.Variable(); 79 | const wrapped = AsyncContext.Snapshot.wrap(() => ctx.get()); 80 | 81 | ctx.run({ id: 1 }, () => { 82 | assert.equal(wrapped(), undefined); 83 | }); 84 | }); 85 | 86 | test("stores current state", () => { 87 | const ctx = new AsyncContext.Variable(); 88 | const expected = { id: 1 }; 89 | 90 | const wrap = ctx.run(expected, () => { 91 | const wrap = AsyncContext.Snapshot.wrap(() => ctx.get()); 92 | assert.equal(wrap(), expected); 93 | assert.equal(ctx.get(), expected); 94 | return wrap; 95 | }); 96 | 97 | assert.equal(wrap(), expected); 98 | assert.equal(ctx.get(), undefined); 99 | }); 100 | 101 | test("runs within wrap", () => { 102 | const ctx = new AsyncContext.Variable(); 103 | const first = { id: 1 }; 104 | const second = { id: 2 }; 105 | 106 | const [wrap1, wrap2] = ctx.run(first, () => { 107 | const wrap1 = AsyncContext.Snapshot.wrap(() => { 108 | assert.equal(ctx.get(), first); 109 | 110 | ctx.run(second, () => { 111 | assert.equal(ctx.get(), second); 112 | }); 113 | 114 | assert.equal(ctx.get(), first); 115 | }); 116 | assert.equal(ctx.get(), first); 117 | 118 | ctx.run(second, () => { 119 | assert.equal(ctx.get(), second); 120 | }); 121 | 122 | const wrap2 = AsyncContext.Snapshot.wrap(() => { 123 | assert.equal(ctx.get(), first); 124 | 125 | ctx.run(second, () => { 126 | assert.equal(ctx.get(), second); 127 | }); 128 | 129 | assert.equal(ctx.get(), first); 130 | }); 131 | assert.equal(ctx.get(), first); 132 | return [wrap1, wrap2]; 133 | }); 134 | 135 | wrap1(); 136 | wrap2(); 137 | assert.equal(ctx.get(), undefined); 138 | }); 139 | 140 | test("runs within wrap", () => { 141 | const ctx = new AsyncContext.Variable(); 142 | const first = { id: 1 }; 143 | const second = { id: 2 }; 144 | 145 | const [wrap1, wrap2] = ctx.run(first, () => { 146 | const wrap1 = AsyncContext.Snapshot.wrap(() => { 147 | assert.equal(ctx.get(), first); 148 | 149 | ctx.run(second, () => { 150 | assert.equal(ctx.get(), second); 151 | }); 152 | 153 | assert.equal(ctx.get(), first); 154 | }); 155 | assert.equal(ctx.get(), first); 156 | 157 | ctx.run(second, () => { 158 | assert.equal(ctx.get(), second); 159 | }); 160 | 161 | const wrap2 = AsyncContext.Snapshot.wrap(() => { 162 | assert.equal(ctx.get(), first); 163 | 164 | ctx.run(second, () => { 165 | assert.equal(ctx.get(), second); 166 | }); 167 | 168 | assert.equal(ctx.get(), first); 169 | }); 170 | assert.equal(ctx.get(), first); 171 | return [wrap1, wrap2]; 172 | }); 173 | 174 | wrap1(); 175 | wrap2(); 176 | assert.equal(ctx.get(), undefined); 177 | }); 178 | 179 | test("runs different context within wrap", () => { 180 | const a = new AsyncContext.Variable(); 181 | const b = new AsyncContext.Variable(); 182 | const first = { id: 1 }; 183 | const second = { id: 2 }; 184 | 185 | const [wrap1, wrap2] = a.run(first, () => { 186 | const wrap1 = AsyncContext.Snapshot.wrap(() => { 187 | assert.equal(a.get(), first); 188 | assert.equal(b.get(), undefined); 189 | 190 | b.run(second, () => { 191 | assert.equal(a.get(), first); 192 | assert.equal(b.get(), second); 193 | }); 194 | 195 | assert.equal(a.get(), first); 196 | assert.equal(b.get(), undefined); 197 | }); 198 | 199 | a.run(second, () => {}); 200 | 201 | const wrap2 = AsyncContext.Snapshot.wrap(() => { 202 | assert.equal(a.get(), first); 203 | assert.equal(b.get(), undefined); 204 | 205 | b.run(second, () => { 206 | assert.equal(a.get(), first); 207 | assert.equal(b.get(), second); 208 | }); 209 | 210 | assert.equal(a.get(), first); 211 | assert.equal(b.get(), undefined); 212 | }); 213 | 214 | assert.equal(a.get(), first); 215 | assert.equal(b.get(), undefined); 216 | return [wrap1, wrap2]; 217 | }); 218 | 219 | wrap1(); 220 | wrap2(); 221 | assert.equal(a.get(), undefined); 222 | assert.equal(b.get(), undefined); 223 | }); 224 | 225 | test("runs different context within wrap, 2", () => { 226 | const a = new AsyncContext.Variable(); 227 | const b = new AsyncContext.Variable(); 228 | const first = { id: 1 }; 229 | const second = { id: 2 }; 230 | 231 | const [wrap1, wrap2] = a.run(first, () => { 232 | const wrap1 = AsyncContext.Snapshot.wrap(() => { 233 | assert.equal(a.get(), first); 234 | assert.equal(b.get(), undefined); 235 | 236 | b.run(second, () => { 237 | assert.equal(a.get(), first); 238 | assert.equal(b.get(), second); 239 | }); 240 | 241 | assert.equal(a.get(), first); 242 | assert.equal(b.get(), undefined); 243 | }); 244 | 245 | b.run(second, () => {}); 246 | 247 | const wrap2 = AsyncContext.Snapshot.wrap(() => { 248 | assert.equal(a.get(), first); 249 | assert.equal(b.get(), undefined); 250 | 251 | b.run(second, () => { 252 | assert.equal(a.get(), first); 253 | assert.equal(b.get(), second); 254 | }); 255 | 256 | assert.equal(a.get(), first); 257 | assert.equal(b.get(), undefined); 258 | }); 259 | 260 | assert.equal(a.get(), first); 261 | assert.equal(b.get(), undefined); 262 | return [wrap1, wrap2]; 263 | }); 264 | 265 | wrap1(); 266 | wrap2(); 267 | assert.equal(a.get(), undefined); 268 | assert.equal(b.get(), undefined); 269 | }); 270 | 271 | test("wrap within nesting contexts", () => { 272 | const ctx = new AsyncContext.Variable(); 273 | const first = { id: 1 }; 274 | const second = { id: 2 }; 275 | 276 | const [firstWrap, secondWrap] = ctx.run(first, () => { 277 | const firstWrap = AsyncContext.Snapshot.wrap(() => { 278 | assert.equal(ctx.get(), first); 279 | }); 280 | firstWrap(); 281 | 282 | const secondWrap = ctx.run(second, () => { 283 | const secondWrap = AsyncContext.Snapshot.wrap(() => { 284 | firstWrap(); 285 | assert.equal(ctx.get(), second); 286 | }); 287 | firstWrap(); 288 | secondWrap(); 289 | assert.equal(ctx.get(), second); 290 | 291 | return secondWrap; 292 | }); 293 | 294 | firstWrap(); 295 | secondWrap(); 296 | assert.equal(ctx.get(), first); 297 | 298 | return [firstWrap, secondWrap]; 299 | }); 300 | 301 | firstWrap(); 302 | secondWrap(); 303 | assert.equal(ctx.get(), undefined); 304 | }); 305 | 306 | test("wrap within nesting different contexts", () => { 307 | const a = new AsyncContext.Variable(); 308 | const b = new AsyncContext.Variable(); 309 | const first = { id: 1 }; 310 | const second = { id: 2 }; 311 | 312 | const [firstWrap, secondWrap] = a.run(first, () => { 313 | const firstWrap = AsyncContext.Snapshot.wrap(() => { 314 | assert.equal(a.get(), first); 315 | assert.equal(b.get(), undefined); 316 | }); 317 | firstWrap(); 318 | 319 | const secondWrap = b.run(second, () => { 320 | const secondWrap = AsyncContext.Snapshot.wrap(() => { 321 | firstWrap(); 322 | assert.equal(a.get(), first); 323 | assert.equal(b.get(), second); 324 | }); 325 | 326 | firstWrap(); 327 | secondWrap(); 328 | assert.equal(a.get(), first); 329 | assert.equal(b.get(), second); 330 | 331 | return secondWrap; 332 | }); 333 | 334 | firstWrap(); 335 | secondWrap(); 336 | assert.equal(a.get(), first); 337 | assert.equal(b.get(), undefined); 338 | 339 | return [firstWrap, secondWrap]; 340 | }); 341 | 342 | firstWrap(); 343 | secondWrap(); 344 | assert.equal(a.get(), undefined); 345 | assert.equal(b.get(), undefined); 346 | }); 347 | 348 | test("wrap within nesting different contexts, 2", () => { 349 | const a = new AsyncContext.Variable(); 350 | const b = new AsyncContext.Variable(); 351 | const c = new AsyncContext.Variable(); 352 | const first = { id: 1 }; 353 | const second = { id: 2 }; 354 | const third = { id: 3 }; 355 | 356 | const wrap = a.run(first, () => { 357 | const wrap = b.run(second, () => { 358 | const wrap = c.run(third, () => { 359 | return AsyncContext.Snapshot.wrap(() => { 360 | assert.equal(a.get(), first); 361 | assert.equal(b.get(), second); 362 | assert.equal(c.get(), third); 363 | }); 364 | }); 365 | assert.equal(a.get(), first); 366 | assert.equal(b.get(), second); 367 | assert.equal(c.get(), undefined); 368 | return wrap; 369 | }); 370 | assert.equal(a.get(), first); 371 | assert.equal(b.get(), undefined); 372 | assert.equal(c.get(), undefined); 373 | 374 | return wrap; 375 | }); 376 | 377 | assert.equal(a.get(), undefined); 378 | assert.equal(b.get(), undefined); 379 | assert.equal(c.get(), undefined); 380 | wrap(); 381 | assert.equal(a.get(), undefined); 382 | assert.equal(b.get(), undefined); 383 | assert.equal(c.get(), undefined); 384 | }); 385 | 386 | test("wrap within nesting different contexts, 3", () => { 387 | const a = new AsyncContext.Variable(); 388 | const b = new AsyncContext.Variable(); 389 | const c = new AsyncContext.Variable(); 390 | const first = { id: 1 }; 391 | const second = { id: 2 }; 392 | const third = { id: 3 }; 393 | 394 | const wrap = a.run(first, () => { 395 | const wrap = b.run(second, () => { 396 | AsyncContext.Snapshot.wrap(() => {}); 397 | 398 | const wrap = c.run(third, () => { 399 | return AsyncContext.Snapshot.wrap(() => { 400 | assert.equal(a.get(), first); 401 | assert.equal(b.get(), second); 402 | assert.equal(c.get(), third); 403 | }); 404 | }); 405 | assert.equal(a.get(), first); 406 | assert.equal(b.get(), second); 407 | assert.equal(c.get(), undefined); 408 | return wrap; 409 | }); 410 | assert.equal(a.get(), first); 411 | assert.equal(b.get(), undefined); 412 | assert.equal(c.get(), undefined); 413 | 414 | return wrap; 415 | }); 416 | 417 | assert.equal(a.get(), undefined); 418 | assert.equal(b.get(), undefined); 419 | assert.equal(c.get(), undefined); 420 | wrap(); 421 | assert.equal(a.get(), undefined); 422 | assert.equal(b.get(), undefined); 423 | assert.equal(c.get(), undefined); 424 | }); 425 | 426 | test("wrap within nesting different contexts, 4", () => { 427 | const a = new AsyncContext.Variable(); 428 | const b = new AsyncContext.Variable(); 429 | const c = new AsyncContext.Variable(); 430 | const first = { id: 1 }; 431 | const second = { id: 2 }; 432 | const third = { id: 3 }; 433 | 434 | const wrap = a.run(first, () => { 435 | AsyncContext.Snapshot.wrap(() => {}); 436 | 437 | const wrap = b.run(second, () => { 438 | const wrap = c.run(third, () => { 439 | return AsyncContext.Snapshot.wrap(() => { 440 | assert.equal(a.get(), first); 441 | assert.equal(b.get(), second); 442 | assert.equal(c.get(), third); 443 | }); 444 | }); 445 | assert.equal(a.get(), first); 446 | assert.equal(b.get(), second); 447 | assert.equal(c.get(), undefined); 448 | return wrap; 449 | }); 450 | assert.equal(a.get(), first); 451 | assert.equal(b.get(), undefined); 452 | assert.equal(c.get(), undefined); 453 | 454 | return wrap; 455 | }); 456 | 457 | assert.equal(a.get(), undefined); 458 | assert.equal(b.get(), undefined); 459 | assert.equal(c.get(), undefined); 460 | wrap(); 461 | assert.equal(a.get(), undefined); 462 | assert.equal(b.get(), undefined); 463 | assert.equal(c.get(), undefined); 464 | }); 465 | 466 | test("wrap within nesting different contexts, 5", () => { 467 | const a = new AsyncContext.Variable(); 468 | const b = new AsyncContext.Variable(); 469 | const c = new AsyncContext.Variable(); 470 | const first = { id: 1 }; 471 | const second = { id: 2 }; 472 | const third = { id: 3 }; 473 | 474 | const wrap = a.run(first, () => { 475 | const wrap = b.run(second, () => { 476 | const wrap = c.run(third, () => { 477 | return AsyncContext.Snapshot.wrap(() => { 478 | assert.equal(a.get(), first); 479 | assert.equal(b.get(), second); 480 | assert.equal(c.get(), third); 481 | }); 482 | }); 483 | 484 | AsyncContext.Snapshot.wrap(() => {}); 485 | 486 | assert.equal(a.get(), first); 487 | assert.equal(b.get(), second); 488 | assert.equal(c.get(), undefined); 489 | return wrap; 490 | }); 491 | assert.equal(a.get(), first); 492 | assert.equal(b.get(), undefined); 493 | assert.equal(c.get(), undefined); 494 | 495 | return wrap; 496 | }); 497 | 498 | assert.equal(a.get(), undefined); 499 | assert.equal(b.get(), undefined); 500 | assert.equal(c.get(), undefined); 501 | wrap(); 502 | assert.equal(a.get(), undefined); 503 | assert.equal(b.get(), undefined); 504 | assert.equal(c.get(), undefined); 505 | }); 506 | 507 | test("wrap within nesting different contexts, 6", () => { 508 | const a = new AsyncContext.Variable(); 509 | const b = new AsyncContext.Variable(); 510 | const c = new AsyncContext.Variable(); 511 | const first = { id: 1 }; 512 | const second = { id: 2 }; 513 | const third = { id: 3 }; 514 | 515 | const wrap = a.run(first, () => { 516 | const wrap = b.run(second, () => { 517 | const wrap = c.run(third, () => { 518 | return AsyncContext.Snapshot.wrap(() => { 519 | assert.equal(a.get(), first); 520 | assert.equal(b.get(), second); 521 | assert.equal(c.get(), third); 522 | }); 523 | }); 524 | assert.equal(a.get(), first); 525 | assert.equal(b.get(), second); 526 | assert.equal(c.get(), undefined); 527 | return wrap; 528 | }); 529 | 530 | AsyncContext.Snapshot.wrap(() => {}); 531 | 532 | assert.equal(a.get(), first); 533 | assert.equal(b.get(), undefined); 534 | assert.equal(c.get(), undefined); 535 | 536 | return wrap; 537 | }); 538 | 539 | assert.equal(a.get(), undefined); 540 | assert.equal(b.get(), undefined); 541 | assert.equal(c.get(), undefined); 542 | wrap(); 543 | assert.equal(a.get(), undefined); 544 | assert.equal(b.get(), undefined); 545 | assert.equal(c.get(), undefined); 546 | }); 547 | 548 | test("wrap out of order", () => { 549 | const ctx = new AsyncContext.Variable(); 550 | const first = { id: 1 }; 551 | const second = { id: 2 }; 552 | 553 | const firstWrap = ctx.run(first, () => { 554 | return AsyncContext.Snapshot.wrap(() => { 555 | assert.equal(ctx.get(), first); 556 | }); 557 | }); 558 | const secondWrap = ctx.run(second, () => { 559 | return AsyncContext.Snapshot.wrap(() => { 560 | assert.equal(ctx.get(), second); 561 | }); 562 | }); 563 | 564 | firstWrap(); 565 | secondWrap(); 566 | firstWrap(); 567 | secondWrap(); 568 | }); 569 | }); 570 | }); 571 | }); 572 | -------------------------------------------------------------------------------- /src/tests/timers.test.ts: -------------------------------------------------------------------------------- 1 | import { AsyncContext } from ".."; 2 | import { wait } from "./_lib"; 3 | 4 | const asyncContext = new AsyncContext.Variable(); 5 | 6 | describe("AsyncContext / setTimeout", () => { 7 | it("should not infere with timers", async () => { 8 | const before = Date.now(); 9 | await new Promise((resolve) => setTimeout(resolve, 100)); 10 | const after = Date.now(); 11 | const timeSpent = after - before; 12 | expect(timeSpent).toBeGreaterThanOrEqual(100); 13 | }); 14 | 15 | it("timer (scenario 1): should know in which context it is", async () => { 16 | const innerCallback = asyncContext.wrap("Inner", async () => { 17 | setTimeout(() => expect(asyncContext.get()).toBe("Inner"), 0); 18 | setTimeout(() => expect(asyncContext.get()).toBe("Inner"), 10); 19 | setTimeout(() => expect(asyncContext.get()).toBe("Inner"), 150); 20 | await wait(100); 21 | setTimeout(() => expect(asyncContext.get()).toBe("Inner"), 0); 22 | setTimeout(() => expect(asyncContext.get()).toBe("Inner"), 100); 23 | }); 24 | 25 | const total = asyncContext.wrap("Outer", async () => { 26 | setTimeout(() => expect(asyncContext.get()).toBe("Outer"), 0); 27 | setTimeout(() => expect(asyncContext.get()).toBe("Outer"), 10); 28 | setTimeout(() => expect(asyncContext.get()).toBe("Outer"), 150); 29 | // console.log('Outer Start') 30 | 31 | // console.log('\t -> Inner Start', SimpleAsyncContext.getStackId()); 32 | const value = await innerCallback(); 33 | 34 | setTimeout(() => expect(asyncContext.get()).toBe("Outer"), 0); 35 | setTimeout(() => expect(asyncContext.get()).toBe("Outer"), 100); 36 | // console.log('\t -> Inner End', SimpleAsyncContext.getStackId()); 37 | // console.log('Outer End'); 38 | return `OUTER ${value}`; 39 | }); 40 | 41 | await total(); 42 | await wait(1000); 43 | }); 44 | 45 | it("timer (scenario 2): should know in which context it is", async () => { 46 | const innerCallback = asyncContext.wrap("Inner", async () => { 47 | setTimeout(() => { 48 | setTimeout(() => { 49 | expect(asyncContext.get()).toBe("Inner"); 50 | }, 0); 51 | }, 0); 52 | setTimeout(() => { 53 | setTimeout(() => { 54 | expect(asyncContext.get()).toBe("Inner"); 55 | }, 10); 56 | }, 150); 57 | 58 | await wait(100); 59 | 60 | setTimeout(() => { 61 | setTimeout(() => { 62 | expect(asyncContext.get()).toBe("Inner"); 63 | }, 10); 64 | }, 150); 65 | 66 | setTimeout(() => { 67 | setTimeout(() => { 68 | expect(asyncContext.get()).toBe("Inner"); 69 | }, 0); 70 | }, 0); 71 | }); 72 | 73 | const total = asyncContext.wrap("Outer", async () => { 74 | setTimeout(() => { 75 | setTimeout(() => { 76 | expect(asyncContext.get()).toBe("Outer"); 77 | }, 0); 78 | }, 0); 79 | setTimeout(() => { 80 | setTimeout(() => { 81 | expect(asyncContext.get()).toBe("Outer"); 82 | }, 10); 83 | }, 150); 84 | // console.log('Outer Start') 85 | 86 | // console.log('\t -> Inner Start', SimpleAsyncContext.getStackId()); 87 | const value = await innerCallback(); 88 | 89 | setTimeout(() => { 90 | setTimeout(() => { 91 | expect(asyncContext.get()).toBe("Outer"); 92 | }, 10); 93 | }, 150); 94 | 95 | setTimeout(() => { 96 | setTimeout(() => { 97 | expect(asyncContext.get()).toBe("Outer"); 98 | }, 0); 99 | }, 0); 100 | // console.log('\t -> Inner End', SimpleAsyncContext.getStackId()); 101 | // console.log('Outer End'); 102 | return `OUTER ${value}`; 103 | }); 104 | 105 | await total(); 106 | await wait(1000); 107 | }); 108 | 109 | it("timer (scenario 3): should know in which context it is", async () => { 110 | const innerCallback = asyncContext.wrap("Inner", async () => { 111 | setTimeout(async () => expect(asyncContext.get()).toBe("Inner"), 0); 112 | setTimeout(async () => { 113 | await wait(200); 114 | expect(asyncContext.get()).toBe("Inner"); 115 | }, 10); 116 | setTimeout(async () => { 117 | await wait(100); 118 | expect(asyncContext.get()).toBe("Inner"); 119 | }, 150); 120 | 121 | await wait(100); 122 | 123 | setTimeout(async () => expect(asyncContext.get()).toBe("Inner"), 0); 124 | setTimeout(async () => { 125 | await wait(200); 126 | expect(asyncContext.get()).toBe("Inner"); 127 | }, 10); 128 | }); 129 | 130 | const total = asyncContext.wrap("Outer", async () => { 131 | setTimeout(async () => expect(asyncContext.get()).toBe("Outer"), 0); 132 | setTimeout(async () => { 133 | await wait(200); 134 | expect(asyncContext.get()).toBe("Outer"); 135 | }, 10); 136 | setTimeout(async () => { 137 | await wait(100); 138 | expect(asyncContext.get()).toBe("Outer"); 139 | }, 150); 140 | // console.log('Outer Start') 141 | 142 | // console.log('\t -> Inner Start', SimpleAsyncContext.getStackId()); 143 | const value = await innerCallback(); 144 | 145 | setTimeout(async () => expect(asyncContext.get()).toBe("Outer"), 0); 146 | setTimeout(async () => { 147 | await wait(200); 148 | expect(asyncContext.get()).toBe("Outer"); 149 | }, 10); 150 | // console.log('\t -> Inner End', SimpleAsyncContext.getStackId()); 151 | // console.log('Outer End'); 152 | return `OUTER ${value}`; 153 | }); 154 | 155 | await total(); 156 | await wait(1000); 157 | }); 158 | }); 159 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["**/tests", "**/*.test.ts", "**/*.test.tsx"] 4 | } 5 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "exclude": ["node_modules"], 3 | "include": ["src/"], 4 | "compilerOptions": { 5 | // match output dir to input dir. e.g. dist/index instead of dist/src/index 6 | "rootDir": "./src", 7 | "outDir": "build", 8 | "allowJs": true, 9 | "module": "commonjs", 10 | "target": "ES6", 11 | "lib": ["ESNext", "dom"], 12 | "importHelpers": true, 13 | "declaration": true, // output .d.ts declaration files for consumers 14 | "sourceMap": true, // output .js.map sourcemap files for consumers 15 | 16 | // use Node's module resolution algorithm, instead of the legacy TS one 17 | "moduleResolution": "node", 18 | 19 | /* Experimental Options */ 20 | "experimentalDecorators": true, 21 | "emitDecoratorMetadata": true, 22 | // transpile JSX to React.createElement 23 | "jsx": "react", 24 | // interop between ESM and CJS modules. Recommended by TS 25 | "esModuleInterop": true, 26 | // significant perf increase by skipping checking .d.ts files, particularly those in node_modules. Recommended by TS 27 | "skipLibCheck": true, 28 | // error out if import and file system have a casing mismatch. Recommended by TS 29 | "forceConsistentCasingInFileNames": true, 30 | "allowSyntheticDefaultImports": true 31 | 32 | } 33 | } 34 | --------------------------------------------------------------------------------