├── .eslintrc.yml ├── .gitignore ├── .prettierrc ├── .travis.yml ├── History.md ├── README.md ├── als.js ├── als.test.js ├── examples └── koa.js ├── package-lock.json ├── package.json └── support └── server.js /.eslintrc.yml: -------------------------------------------------------------------------------- 1 | extends: 2 | - airbnb-base 3 | - prettier 4 | parserOptions: 5 | ecmaVersion: 2017 6 | sourceType: module 7 | rules: 8 | no-console: 0 9 | import/no-unresolved: 0 10 | import/extensions: 11 | - .mjs 12 | - .js 13 | - .json 14 | globals: 15 | test: true 16 | expect: true 17 | describe: true 18 | afterAll: true 19 | beforeAll: true 20 | beforeEach: true 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | tmp 4 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "bracketSpacing": false, 3 | "jsxBracketSameLine": true, 4 | "parser": "flow", 5 | "printWidth": 80, 6 | "singleQuote": true, 7 | "trailingComma": "all" 8 | } -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - "12" 5 | - "11" 6 | - "10" 7 | - "9" 8 | - "8" 9 | 10 | services: 11 | - docker 12 | 13 | before_install: 14 | - docker run -d --restart=always -p 6379:6379 --name=redis redis:alpine 15 | script: 16 | - npm test 17 | 18 | after_script: 19 | - npm install coveralls@2 && cat ./coverage/lcov.info | coveralls 20 | -------------------------------------------------------------------------------- /History.md: -------------------------------------------------------------------------------- 1 | # 2.3.0 2 | 3 | * support enable/disable create time for save memory 4 | 5 | # 2.2.0 6 | 7 | * add getAllData function 8 | 9 | # 2.1.0 10 | 11 | * Support getParentData 12 | 13 | # 1.2.0 14 | 15 | * Support to setting all the data in top parent #6 16 | 17 | # 1.1.1 18 | 19 | * Fix the context is lost in process next tick 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # async-local-storage 2 | 3 | !!!Please use [AsyncLocalStorage](https://nodejs.org/dist/latest-v16.x/docs/api/async_hooks.html#async_hooks_class_asynclocalstorage) instead of async-local-storage. 4 | 5 | [![Build Status](https://travis-ci.org/vicanso/async-local-storage.svg?branch=master)](https://travis-ci.org/vicanso/async-local-storage) 6 | [![Coverage Status](https://img.shields.io/coveralls/vicanso/async-local-storage/master.svg?style=flat)](https://coveralls.io/r/vicanso/async-local-storage?branch=master) 7 | [![npm](http://img.shields.io/npm/v/async-local-storage.svg?style=flat-square)](https://www.npmjs.org/package/async-local-storage) 8 | [![Github Releases](https://img.shields.io/npm/dm/async-local-storage.svg?style=flat-square)](https://github.com/vicanso/async-local-storage) 9 | 10 | I want something like `thread-local` storage in threaded programming and `async_hooks` is usable in `node.js 8.0`, so there is an easy way to use `thread-local`. 11 | 12 | ## API 13 | 14 | ```js 15 | const als = require('async-local-storage'); 16 | als.enable(); 17 | setTimeout(() => { 18 | als.scope(); 19 | const id = randomBytes(8); 20 | als.set('id', id); 21 | delay().then(() => { 22 | assert.equal(als.get('id'), id); 23 | return readfilePromise(__filename); 24 | }).then(() => { 25 | assert.equal(als.get('id'), id); 26 | return superagent.get('http://www.baidu.com/'); 27 | }).then(() => { 28 | assert.equal(als.get('id'), id); 29 | }); 30 | }, 100); 31 | ``` 32 | 33 | ### enable 34 | 35 | enable the async hooks 36 | 37 | ```js 38 | const als = require('async-local-storage'); 39 | als.enable(); 40 | ``` 41 | 42 | ### disable 43 | 44 | disable the async hooks 45 | 46 | ```js 47 | const als = require('async-local-storage'); 48 | als.enable(); 49 | setTimeout(() => { 50 | als.disable(); 51 | }, 100); 52 | ``` 53 | 54 | ### size 55 | 56 | get the size of storage 57 | 58 | ```js 59 | const als = require('async-local-storage'); 60 | als.enable(); 61 | setTimeout(() => { 62 | console.info(als.size()); 63 | }, 100); 64 | ``` 65 | 66 | ### scope 67 | 68 | change the scope of call chain, it will be the call chain top (remove the parent of itself) 69 | 70 | ```js 71 | const als = require('async-local-storage'); 72 | const Koa = require('koa'); 73 | const assert = require('assert'); 74 | 75 | const app = new Koa(); 76 | app.use(async (ctx, next) => { 77 | const id = ctx.get('X-Request-Id'); 78 | als.scope(); 79 | als.set('id', id); 80 | await next(); 81 | }); 82 | 83 | app.use(async (ctx, next) => { 84 | const id = ctx.get('X-Request-Id'); 85 | assert.equal(als.get('id'), id); 86 | await next(); 87 | }); 88 | 89 | app.use((ctx) => { 90 | ctx.body = 'OK'; 91 | }); 92 | ``` 93 | 94 | 95 | ### set 96 | 97 | set the value by key for the current id 98 | 99 | - `key` the key 100 | - `value` the value 101 | - `linkedTop` set the value linked to top 102 | 103 | ```js 104 | als.enable() 105 | setTimeout(() => { 106 | als.scope(); 107 | const id = randomBytes(); 108 | setTimeout(() => { 109 | als.set('id', id, true); 110 | }, 1); 111 | setTimeout(() => { 112 | assert.equal(als.get('id'), id); 113 | }, 10); 114 | }, 10); 115 | ``` 116 | 117 | ### get 118 | 119 | get the value by key, if will find from parent, self --> parent --> parent, until the value is not undefined 120 | 121 | - `key` the key 122 | 123 | ```js 124 | als.enable(); 125 | setTimeout(() => { 126 | als.scope(); 127 | const id = randomBytes(); 128 | setTimeout(() => { 129 | als.set('id', id, true); 130 | }, 1); 131 | setTimeout(() => { 132 | assert.equal(als.get('id'), id); 133 | }, 10); 134 | }, 10); 135 | ``` 136 | 137 | ### enableLinkedTop 138 | 139 | enable linked top for default (default is disabled) 140 | 141 | ```js 142 | als.enable(); 143 | als.enableLinkedTop(); 144 | setTimeout(() => { 145 | als.scope(); 146 | setTimeout(() => { 147 | // the same as als.set('id', 'a', true) 148 | als.set('id', 'a'); 149 | }, 10); 150 | }, 10); 151 | ``` 152 | 153 | ### disableLinkedTop 154 | 155 | disable linked top for default 156 | 157 | ```js 158 | als.enable(); 159 | als.enableLinkedTop(); 160 | setTimeout(() => { 161 | als.disableLinkedTop(); 162 | als.scope(); 163 | setTimeout(() => { 164 | // the same as als.set('id', 'a', false) 165 | als.set('id', 'a'); 166 | }, 10); 167 | }, 10); 168 | ``` 169 | 170 | 171 | ### currentId 172 | 173 | get the current id 174 | 175 | ```js 176 | const assert = require('assert'); 177 | als.enable(); 178 | setTimeout(() => { 179 | console.info(als.currentId()); 180 | }, 10); 181 | ``` 182 | 183 | ### use 184 | 185 | get the use time of id 186 | 187 | - `id` The tigger id, default is `als.currentId()` 188 | 189 | ```js 190 | als.enable() 191 | setTimeout(() => { 192 | const id = als.currentId(); 193 | console.info(als.use(id)); 194 | }, 10); 195 | ``` 196 | 197 | ### enableCreateTime 198 | 199 | enable create time of data, default is enabled. 200 | 201 | ```js 202 | als.enableCreateTime(); 203 | ``` 204 | 205 | ### disableCreateTime 206 | 207 | disable create time of data, it can save memory. 208 | 209 | ```js 210 | als.disableCreateTime(); 211 | ``` 212 | -------------------------------------------------------------------------------- /als.js: -------------------------------------------------------------------------------- 1 | const asyncHooks = require('async_hooks'); 2 | const nano = require('nano-seconds'); 3 | const util = require('util'); 4 | const fs = require('fs'); 5 | 6 | const map = new Map(); 7 | 8 | const enabledDebug = process.env.DEBUG === 'als'; 9 | 10 | function debug(...args) { 11 | if (!enabledDebug) { 12 | return; 13 | } 14 | // use a function like this one when debugging inside an AsyncHooks callback 15 | fs.writeSync(1, `${util.format(...args)}\n`); 16 | } 17 | 18 | let defaultLinkedTop = false; 19 | let enabledCreatedAt = true; 20 | 21 | function isUndefined(value) { 22 | return value === undefined; 23 | } 24 | 25 | /** 26 | * Get data from itself or parent 27 | * @param {any} data The map data 28 | * @param {any} key The key 29 | * @returns {any} 30 | */ 31 | function get(data, key) { 32 | /* istanbul ignore if */ 33 | if (!data) { 34 | return null; 35 | } 36 | let currentData = data; 37 | let value = currentData[key]; 38 | while (isUndefined(value) && currentData.parent) { 39 | currentData = currentData.parent; 40 | value = currentData[key]; 41 | } 42 | return value; 43 | } 44 | 45 | /** 46 | * Get the top data 47 | */ 48 | function getTop(data) { 49 | let result = data; 50 | while (result && result.parent) { 51 | result = result.parent; 52 | } 53 | return result; 54 | } 55 | 56 | let currentId = 0; 57 | const hooks = asyncHooks.createHook({ 58 | init: function init(id, type, triggerId) { 59 | const data = {}; 60 | // init, set the created time 61 | if (enabledCreatedAt) { 62 | data.created = nano.now(); 63 | } 64 | const parentId = triggerId || currentId; 65 | // not trigger by itself, add parent 66 | if (parentId !== id) { 67 | const parent = map.get(parentId); 68 | if (parent) { 69 | data.parent = parent; 70 | } 71 | } 72 | debug(`${id}(${type}) init by ${triggerId}`); 73 | map.set(id, data); 74 | }, 75 | /** 76 | * Set the current id 77 | */ 78 | before: function before(id) { 79 | currentId = id; 80 | }, 81 | /** 82 | * Remove the data 83 | */ 84 | destroy: function destroy(id) { 85 | if (!map.has(id)) { 86 | return; 87 | } 88 | debug(`destroy ${id}`); 89 | map.delete(id); 90 | }, 91 | }); 92 | 93 | /** 94 | * Get the current id 95 | */ 96 | function getCurrentId() { 97 | if (asyncHooks.executionAsyncId) { 98 | return asyncHooks.executionAsyncId(); 99 | } 100 | return asyncHooks.currentId() || currentId; 101 | } 102 | 103 | /** 104 | * Get the current id 105 | */ 106 | exports.currentId = getCurrentId; 107 | 108 | /** 109 | * Enable the async hook 110 | */ 111 | exports.enable = () => hooks.enable(); 112 | 113 | /** 114 | * Disable the async hook 115 | */ 116 | exports.disable = () => hooks.disable(); 117 | 118 | /** 119 | * Get the size of map 120 | */ 121 | exports.size = () => map.size; 122 | 123 | /** 124 | * Enable linked top 125 | */ 126 | exports.enableLinkedTop = () => { 127 | defaultLinkedTop = true; 128 | }; 129 | 130 | /** 131 | * Disable linked top 132 | */ 133 | exports.disableLinkedTop = () => { 134 | defaultLinkedTop = false; 135 | }; 136 | 137 | /** 138 | * Set the key/value for this score 139 | * @param {String} key The key of value 140 | * @param {String} value The value 141 | * @param {Boolean} linkedTop The value linked to top 142 | * @returns {Boolean} if success, will return true, otherwise false 143 | */ 144 | exports.set = function setValue(key, value, linkedTop) { 145 | /* istanbul ignore if */ 146 | if (key === 'created' || key === 'parent') { 147 | throw new Error("can't set created and parent"); 148 | } 149 | const id = getCurrentId(); 150 | debug(`set ${key}:${value} to ${id}`); 151 | let data = map.get(id); 152 | /* istanbul ignore if */ 153 | if (!data) { 154 | return false; 155 | } 156 | let setToLinkedTop = linkedTop; 157 | if (isUndefined(linkedTop)) { 158 | setToLinkedTop = defaultLinkedTop; 159 | } 160 | if (setToLinkedTop) { 161 | data = getTop(data); 162 | } 163 | data[key] = value; 164 | return true; 165 | }; 166 | 167 | /** 168 | * Get the value by key 169 | * @param {String} key The key of value 170 | */ 171 | exports.get = function getValue(key) { 172 | const data = map.get(getCurrentId()); 173 | const value = get(data, key); 174 | debug(`get ${key}:${value} from ${currentId}`); 175 | return value; 176 | }; 177 | 178 | /** 179 | * 获取当前current data 180 | */ 181 | exports.getCurrentData = () => map.get(getCurrentId()); 182 | 183 | /** 184 | * Get the value by key from parent 185 | * @param {String} key The key of value 186 | */ 187 | exports.getFromParent = key => { 188 | const currentData = map.get(getCurrentId()); 189 | if (!currentData) { 190 | return null; 191 | } 192 | const value = get({parent: currentData.parent}, key); 193 | return value; 194 | }; 195 | 196 | /** 197 | * Remove the data of the current id 198 | */ 199 | exports.remove = function removeValue() { 200 | const id = getCurrentId(); 201 | if (id) { 202 | map.delete(id); 203 | } 204 | }; 205 | 206 | /** 207 | * Get the use the of id 208 | * @param {Number} id The trigger id, is optional, default is `als.currentId()` 209 | * @returns {Number} The use time(ns) of the current id 210 | */ 211 | exports.use = function getUse(id) { 212 | const data = map.get(id || getCurrentId()); 213 | /* istanbul ignore if */ 214 | if (!data || !enabledCreatedAt) { 215 | return -1; 216 | } 217 | return nano.difference(data.created); 218 | }; 219 | 220 | /** 221 | * Get the top value 222 | */ 223 | exports.top = function top() { 224 | const data = map.get(getCurrentId()); 225 | return getTop(data); 226 | }; 227 | 228 | /** 229 | * Set the scope (it will change the top) 230 | */ 231 | exports.scope = function scope() { 232 | const data = map.get(getCurrentId()); 233 | delete data.parent; 234 | }; 235 | 236 | /** 237 | * Get all data of async locatl storage, please don't modify the data 238 | */ 239 | exports.getAllData = function getAllData() { 240 | return map; 241 | }; 242 | 243 | /** 244 | * Enable the create time of data 245 | */ 246 | exports.enableCreateTime = function enableCreateTime() { 247 | enabledCreatedAt = true; 248 | }; 249 | 250 | /** 251 | * Disable the create time of data 252 | */ 253 | exports.disableCreateTime = function disableCreateTime() { 254 | enabledCreatedAt = false; 255 | }; 256 | -------------------------------------------------------------------------------- /als.test.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const fs = require('fs'); 3 | const util = require('util'); 4 | const crypto = require('crypto'); 5 | const dns = require('dns'); 6 | const http = require('http'); 7 | const request = require('supertest'); 8 | 9 | const als = require('./als'); 10 | const {server, redisClient} = require('./support/server'); 11 | 12 | const topList = []; 13 | 14 | const delay = ms => new Promise(resolve => setTimeout(resolve, ms)); 15 | const randomBytes = () => crypto.randomBytes(8).toString('hex'); 16 | const writeFile = util.promisify(fs.writeFile); 17 | const readFile = util.promisify(fs.readFile); 18 | const lookup = util.promisify(dns.lookup); 19 | const lookupService = util.promisify(dns.lookupService); 20 | const file = path.join(__dirname, './tmp'); 21 | fs.writeFileSync(file, ''); 22 | 23 | als.enable(); 24 | 25 | afterAll(() => { 26 | als.disable(); 27 | redisClient.disconnect(); 28 | server.close(); 29 | const ids = []; 30 | let err = null; 31 | topList.forEach(item => { 32 | if (ids.indexOf(item.id) !== -1) { 33 | err = new Error('the id should be unique'); 34 | } else { 35 | ids.push(item.id); 36 | } 37 | }); 38 | if (err) { 39 | console.error(err); 40 | throw err; 41 | } 42 | }); 43 | 44 | describe('fs', () => { 45 | test('fs watch', async () => { 46 | als.scope(); 47 | const id = randomBytes(); 48 | als.set('id', id); 49 | const watcher = fs.watch(file, () => { 50 | expect(als.get('id')).toBe(id); 51 | watcher.close(); 52 | }); 53 | await writeFile(file, 'a'); 54 | topList.push(als.top()); 55 | }); 56 | 57 | test('fs write/read promise', () => { 58 | als.scope(); 59 | const id = randomBytes(); 60 | als.set('id', id); 61 | return writeFile(file, 'a') 62 | .then(() => { 63 | expect(als.get('id')).toBe(id); 64 | return new Promise((resolve, reject) => { 65 | fs.readFile(file, err => { 66 | if (err) { 67 | reject(err); 68 | return; 69 | } 70 | expect(als.get('id')).toBe(id); 71 | resolve(); 72 | }); 73 | }); 74 | }) 75 | .then(() => { 76 | topList.push(als.top()); 77 | }); 78 | }); 79 | 80 | test('fs write/read async/await', async () => { 81 | als.scope(); 82 | const id = randomBytes(); 83 | als.set('id', id); 84 | await writeFile(file, 'a'); 85 | expect(als.get('id')).toBe(id); 86 | await readFile(file); 87 | expect(als.get('id')).toBe(id); 88 | topList.push(als.top()); 89 | }); 90 | }); 91 | 92 | describe('JSSTREAM', () => { 93 | test('js read stream', () => 94 | new Promise((resolve, reject) => { 95 | als.scope(); 96 | const id = randomBytes(); 97 | als.set('id', id); 98 | const stream = fs.createReadStream(file); 99 | stream.on('data', () => { 100 | expect(als.get('id')).toBe(id); 101 | stream.close(); 102 | }); 103 | stream.on('error', reject); 104 | stream.on('close', () => { 105 | topList.push(als.top()); 106 | expect(als.get('id')).toBe(id); 107 | resolve(); 108 | }); 109 | fs.writeFileSync(file, 'ab'); 110 | })); 111 | 112 | test('js write stream', () => 113 | new Promise((resolve, reject) => { 114 | als.scope(); 115 | const id = randomBytes(); 116 | als.set('id', id); 117 | const stream = fs.createWriteStream(file); 118 | stream.on('error', reject); 119 | stream.on('close', () => { 120 | topList.push(als.top()); 121 | expect(als.get('id')).toBe(id); 122 | resolve(); 123 | }); 124 | stream.write('ab'); 125 | stream.end(); 126 | })); 127 | }); 128 | 129 | describe('dns', () => { 130 | test('get addr info promise', () => { 131 | als.scope(); 132 | const id = randomBytes(); 133 | als.set('id', id); 134 | return lookup('www.baidu.com') 135 | .then(() => { 136 | expect(als.get('id')).toBe(id); 137 | }) 138 | .then(() => { 139 | topList.push(als.top()); 140 | }); 141 | }); 142 | 143 | test('get addr info async/await', async () => { 144 | als.scope(); 145 | const id = randomBytes(); 146 | als.set('id', id); 147 | await lookup('www.baidu.com'); 148 | expect(als.get('id')).toBe(id); 149 | topList.push(als.top()); 150 | }); 151 | 152 | test('get name info promise', () => { 153 | als.scope(); 154 | const id = randomBytes(); 155 | als.set('id', id); 156 | return lookupService('127.0.0.1', 80) 157 | .then(() => { 158 | expect(als.get('id')).toBe(id); 159 | }) 160 | .then(() => { 161 | topList.push(als.top()); 162 | }); 163 | }); 164 | 165 | test('get name info async/await', async () => { 166 | als.scope(); 167 | const id = randomBytes(); 168 | als.set('id', id); 169 | await lookupService('127.0.0.1', 80); 170 | expect(als.get('id')).toBe(id); 171 | topList.push(als.top()); 172 | }); 173 | }); 174 | 175 | describe('http', () => { 176 | test('http get', () => { 177 | als.scope(); 178 | const id = randomBytes(); 179 | als.set('id', id); 180 | return new Promise((resolve, reject) => { 181 | expect(als.get('id')).toBe(id); 182 | http 183 | .get('http://www.baidu.com/', res => { 184 | expect(als.get('id')).toBe(id); 185 | res.on('data', () => { 186 | expect(als.get('id')).toBe(id); 187 | }); 188 | res.on('end', () => { 189 | expect(als.get('id')).toBe(id); 190 | resolve(); 191 | }); 192 | }) 193 | .on('error', reject); 194 | }).then(() => { 195 | expect(als.get('id')).toBe(id); 196 | topList.push(als.top()); 197 | }); 198 | }); 199 | }); 200 | 201 | describe('linked top', () => { 202 | test('normal linked top', () => { 203 | als.scope(); 204 | const id = randomBytes(); 205 | const user = randomBytes(); 206 | als.set('id', id); 207 | return delay(10) 208 | .then(() => { 209 | const fn1 = delay(10).then(() => { 210 | const current = als.getCurrentData(); 211 | expect(current.user).toBeUndefined(); 212 | }); 213 | const fn2 = new Promise(resolve => { 214 | als.set('user', user); 215 | resolve(); 216 | }); 217 | return Promise.all([fn1, fn2]); 218 | }) 219 | .then(() => { 220 | expect(als.get('id')).toBe(id); 221 | expect(als.get('user')).toBe(user); 222 | }); 223 | }); 224 | 225 | test('set value to self scope', () => { 226 | als.scope(); 227 | const id = randomBytes(); 228 | const user = randomBytes(); 229 | als.set('id', id); 230 | return delay(10) 231 | .then(() => { 232 | expect(als.get('id')).toBe(id); 233 | als.set('id', 'a'); 234 | als.set('user', user); 235 | }) 236 | .then(() => { 237 | expect(als.get('id')).toBe('a'); 238 | expect(als.get('user')).toBe(user); 239 | }); 240 | }); 241 | 242 | test('enableLinkedTop', () => { 243 | als.scope(); 244 | const id = randomBytes(); 245 | const user = randomBytes(); 246 | als.enableLinkedTop(); 247 | als.set('id', id); 248 | const current = als.getCurrentData(); 249 | return delay(10) 250 | .then(() => { 251 | als.set('user', user); 252 | }) 253 | .then(() => { 254 | expect(current.id).toBe(id); 255 | expect(current.user).toBe(user); 256 | als.disableLinkedTop(); 257 | }); 258 | }); 259 | }); 260 | 261 | describe('get use', () => { 262 | test('use', () => { 263 | als.scope(); 264 | let id = als.currentId(); 265 | return delay(10).then(() => { 266 | const use = als.use(id); 267 | expect(use).toBeGreaterThanOrEqual(9 * 1000 * 1000); 268 | id = als.currentId(); 269 | als.disableCreateTime(); 270 | return delay(10); 271 | }).then(() => { 272 | const use = als.use(id); 273 | expect(use).toBe(-1); 274 | }); 275 | }); 276 | }); 277 | 278 | describe('size', () => { 279 | test('get size', () => { 280 | expect(als.size()).toBeGreaterThan(1); 281 | }); 282 | }); 283 | 284 | describe('koa', () => { 285 | const check = url => { 286 | const fns = [1, 2, 3, 4, 5].map(() => { 287 | const id = randomBytes(8); 288 | return request(server) 289 | .get(url) 290 | .set('X-Request-Id', id) 291 | .expect(200) 292 | .then(response => { 293 | expect(response.text).toBe(id); 294 | }); 295 | }); 296 | return Promise.all(fns); 297 | }; 298 | test('get request id', () => check('/')); 299 | 300 | test('get request id (fs)', () => check('/?fs=true')); 301 | 302 | test('get request id(delay)', () => check('/?delay=true')); 303 | 304 | test('get request id(http)', () => check('/?http=true')); 305 | 306 | test('get request id(session)', () => check('/?session=true')); 307 | 308 | test('get request id(all)', () => 309 | check('/?fs=true&delay=true&http=true&session=true')); 310 | }); 311 | 312 | describe('getFromParent', () => { 313 | test('top', () => { 314 | als.scope(); 315 | let id = als.getFromParent('id'); 316 | expect(id).toBe(undefined); 317 | als.set('id', 1); 318 | id = als.getFromParent('id'); 319 | expect(id).toBe(undefined); 320 | }); 321 | describe('1 level', () => { 322 | test('single', () => { 323 | als.scope(); 324 | als.set('id', 1); 325 | return delay(10).then(() => { 326 | expect(als.getFromParent('id')).toBe(1); 327 | als.set('id', 2); 328 | expect(als.getFromParent('id')).toBe(1); 329 | expect(als.get('id')).toBe(2); 330 | }); 331 | }); 332 | test('multiple', () => { 333 | als.scope(); 334 | als.set('id', 1); 335 | return Promise.all([ 336 | delay(10).then(() => { 337 | expect(als.getFromParent('id')).toBe(1); 338 | als.set('id', 2); 339 | expect(als.getFromParent('id')).toBe(1); 340 | expect(als.get('id')).toBe(2); 341 | }), 342 | delay(10).then(() => { 343 | expect(als.getFromParent('id')).toBe(1); 344 | als.set('id', 3); 345 | expect(als.getFromParent('id')).toBe(1); 346 | expect(als.get('id')).toBe(3); 347 | }), 348 | ]).then(() => { 349 | expect(als.get('id')).toBe(1); 350 | }); 351 | }); 352 | }); 353 | test('2 level', () => { 354 | als.scope(); 355 | als.set('id', 1); 356 | return delay(10).then(() => { 357 | als.set('key2', 1); 358 | return delay(10).then(() => { 359 | expect(als.get('key2')).toBe(1); 360 | expect(als.getFromParent('id')).toBe(1); 361 | als.set('id', 2); 362 | expect(als.getFromParent('id')).toBe(1); 363 | expect(als.get('id')).toBe(2); 364 | }); 365 | }); 366 | }); 367 | }); 368 | 369 | describe('get all data', () => { 370 | expect(als.getAllData()).toBeDefined(); 371 | }); 372 | -------------------------------------------------------------------------------- /examples/koa.js: -------------------------------------------------------------------------------- 1 | const Koa = require('koa'); 2 | const crypto = require('crypto'); 3 | const request = require('superagent'); 4 | const EventEmitter = require('events'); 5 | const assert = require('assert'); 6 | 7 | const als = require('..'); 8 | 9 | const randomBytes = () => crypto.randomBytes(8).toString('hex'); 10 | const emiter = new EventEmitter(); 11 | 12 | 13 | const app = new Koa(); 14 | const key = 'id'; 15 | als.enable(); 16 | 17 | setInterval(() => { 18 | console.info(`map size:${als.size()}`); 19 | }, 60 * 1000); 20 | 21 | app.use((ctx, next) => { 22 | const id = randomBytes(); 23 | ctx.state[key] = id; 24 | als.scope(); 25 | als.set(key, id); 26 | return next(); 27 | }); 28 | 29 | app.use((ctx, next) => { 30 | function done() { 31 | assert.equal(ctx.state[key], als.get(key)); 32 | } 33 | return next().then(done, done); 34 | }); 35 | app.use((ctx, next) => { 36 | setTimeout(() => { 37 | assert.equal(ctx.state[key], als.get(key)); 38 | }, 10); 39 | return next(); 40 | }); 41 | app.use((ctx, next) => { 42 | setImmediate(() => { 43 | als.set('immediate', true, true); 44 | assert.equal(ctx.state[key], als.get(key)); 45 | }); 46 | return next(); 47 | }); 48 | 49 | app.use((ctx, next) => { 50 | process.nextTick(() => { 51 | assert.equal(ctx.state[key], als.get(key)); 52 | }); 53 | return next(); 54 | }); 55 | app.use((ctx, next) => { 56 | ctx.res.once('finish', () => { 57 | // 在finish触发时,调用链已经被删除 58 | // fail!!! 59 | // the finish event call can not get the name from als 60 | assert.equal(als.get(key), undefined); 61 | }); 62 | return next(); 63 | }); 64 | app.use((ctx, next) => { 65 | emiter.once('my-event', () => { 66 | // 此事件触发的时候,调用链还存在 67 | assert.equal(ctx.state[key], als.get(key)); 68 | }); 69 | return next(); 70 | }); 71 | 72 | app.use(async (ctx, next) => { 73 | await request.get('https://www.baidu.com/'); 74 | assert.equal(ctx.state[key], als.get(key)); 75 | return next(); 76 | }); 77 | 78 | app.use((ctx, next) => { 79 | const url = 'https://www.baidu.com/'; 80 | return request.get(url) 81 | .then(() => { 82 | assert.equal(true, als.get('immediate')); 83 | assert.equal(ctx.state[key], als.get(key)); 84 | return next(); 85 | }); 86 | }); 87 | 88 | app.use((ctx) => { 89 | emiter.emit('my-event'); 90 | ctx.body = `Hello ${als.get(key)}`; 91 | }); 92 | 93 | app.listen(3015); 94 | console.info('http://127.0.0.1:3015/'); 95 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "async-local-storage", 3 | "description": "Get the value like thread-local storage in threaded programming", 4 | "version": "2.3.1", 5 | "author": "Tree Xie ", 6 | "keywords": [ 7 | "threadlocal", 8 | "call chain" 9 | ], 10 | "scripts": { 11 | "size": "t=\"$(npm pack .)\"; wc -c \"${t}\"; tar tvf \"${t}\"; rm \"${t}\";", 12 | "commitmsg": "validate-commit-msg", 13 | "precommit": "npm run lint", 14 | "format": "node node_modules/.bin/prettier --write *.js", 15 | "jest": "node node_modules/.bin/jest --coverage *.test.js", 16 | "lint": "node ./node_modules/.bin/eslint *.js", 17 | "test": "npm run lint && npm run jest" 18 | }, 19 | "license": "MIT", 20 | "repository": { 21 | "type": "git", 22 | "url": "https://github.com/vicanso/async-local-storage.git" 23 | }, 24 | "main": "./als.js", 25 | "engines": { 26 | "node": ">=8.0.0" 27 | }, 28 | "devDependencies": { 29 | "eslint": "^4.17.0", 30 | "eslint-config-airbnb-base": "^12.1.0", 31 | "eslint-config-prettier": "^2.9.0", 32 | "eslint-plugin-import": "^2.8.0", 33 | "husky": "^0.14.3", 34 | "ioredis": "^3.2.2", 35 | "jest": "^24.8.0", 36 | "koa": "^2.5.1", 37 | "koa-session": "^5.8.1", 38 | "prettier": "^1.10.2", 39 | "superagent": "^3.8.3", 40 | "supertest": "^3.1.0", 41 | "validate-commit-msg": "^2.14.0" 42 | }, 43 | "dependencies": { 44 | "nano-seconds": "^1.2.2" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /support/server.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const Koa = require('koa'); 3 | const Redis = require('ioredis'); 4 | const superagent = require('superagent'); 5 | const util = require('util'); 6 | const koaSession = require('koa-session'); 7 | const fs = require('fs'); 8 | 9 | const als = require('../als'); 10 | 11 | const readfilePromise = util.promisify(fs.readFile); 12 | als.enable(); 13 | 14 | const delay = ms => new Promise(resolve => setTimeout(resolve, ms)); 15 | 16 | 17 | class SessionStore { 18 | constructor(redisClient) { 19 | this.redisClient = redisClient; 20 | } 21 | async get(key) { 22 | const data = await this.redisClient.get(key); 23 | if (!data) { 24 | return null; 25 | } 26 | return JSON.parse(data); 27 | } 28 | async set(key, json, maxAge) { 29 | await this.redisClient.psetex(key, maxAge, JSON.stringify(json)); 30 | } 31 | async destroy(key) { 32 | await this.redisClient.del(key); 33 | } 34 | } 35 | 36 | const app = new Koa(); 37 | const redisClient = new Redis(6379, '127.0.0.1'); 38 | 39 | const sessionMiddleware = koaSession(app, { 40 | store: new SessionStore(redisClient), 41 | }); 42 | 43 | app.use(async (ctx, next) => { 44 | const id = ctx.get('X-Request-Id'); 45 | als.scope(); 46 | als.set('id', id); 47 | await next(); 48 | assert(als.currentId()); 49 | }); 50 | 51 | // fs 52 | app.use(async (ctx, next) => { 53 | assert.equal(als.get('id'), ctx.get('X-Request-Id')); 54 | if (ctx.query.fs) { 55 | const buf = await readfilePromise(__filename); 56 | als.set('buf', buf); 57 | } 58 | assert(als.currentId()); 59 | return next(); 60 | }); 61 | 62 | // delay 63 | app.use(async (ctx, next) => { 64 | assert.equal(als.get('id'), ctx.get('X-Request-Id')); 65 | if (ctx.query.delay) { 66 | await delay(100); 67 | } 68 | assert(als.currentId()); 69 | return next(); 70 | }); 71 | 72 | // next tick 73 | app.use(async (ctx, next) => { 74 | await new Promise((resolve) => { 75 | process.nextTick(() => { 76 | assert.equal(als.get('id'), ctx.get('X-Request-Id')); 77 | resolve(); 78 | }); 79 | }); 80 | return next(); 81 | }); 82 | 83 | // session 84 | app.use(async (ctx, next) => { 85 | assert.equal(als.get('id'), ctx.get('X-Request-Id')); 86 | if (ctx.query.session) { 87 | return sessionMiddleware(ctx, next); 88 | } 89 | assert(als.currentId()); 90 | return next(); 91 | }); 92 | 93 | // http 94 | app.use(async (ctx, next) => { 95 | assert.equal(als.get('id'), ctx.get('X-Request-Id')); 96 | if (ctx.query.http) { 97 | return superagent.get('http://www.baidu.com/').then(() => { 98 | assert.equal(als.get('id'), ctx.get('X-Request-Id')); 99 | return next(); 100 | }); 101 | } 102 | assert(als.currentId()); 103 | return next(); 104 | }); 105 | 106 | app.use((ctx) => { 107 | if (ctx.query.session) { 108 | assert(ctx.session); 109 | } 110 | if (ctx.query.fs) { 111 | assert(als.get('buf')); 112 | } 113 | assert(als.currentId()); 114 | ctx.body = als.get('id'); 115 | }); 116 | 117 | const server = app.listen(); 118 | 119 | exports.server = server; 120 | exports.redisClient = redisClient; 121 | --------------------------------------------------------------------------------