1'use strict'; 2 3const { 4 NumberIsSafeInteger, 5 ObjectDefineProperties, 6 ObjectIs, 7 ReflectApply, 8 Symbol, 9} = primordials; 10 11const { 12 ERR_ASYNC_CALLBACK, 13 ERR_ASYNC_TYPE, 14 ERR_INVALID_ARG_TYPE, 15 ERR_INVALID_ASYNC_ID 16} = require('internal/errors').codes; 17const { validateString } = require('internal/validators'); 18const internal_async_hooks = require('internal/async_hooks'); 19 20// Get functions 21// For userland AsyncResources, make sure to emit a destroy event when the 22// resource gets gced. 23const { registerDestroyHook } = internal_async_hooks; 24const { 25 executionAsyncId, 26 triggerAsyncId, 27 // Private API 28 hasAsyncIdStack, 29 getHookArrays, 30 enableHooks, 31 disableHooks, 32 executionAsyncResource, 33 // Internal Embedder API 34 newAsyncId, 35 getDefaultTriggerAsyncId, 36 emitInit, 37 emitBefore, 38 emitAfter, 39 emitDestroy, 40 enabledHooksExist, 41 initHooksExist, 42 destroyHooksExist, 43} = internal_async_hooks; 44 45// Get symbols 46const { 47 async_id_symbol, trigger_async_id_symbol, 48 init_symbol, before_symbol, after_symbol, destroy_symbol, 49 promise_resolve_symbol 50} = internal_async_hooks.symbols; 51 52// Get constants 53const { 54 kInit, kBefore, kAfter, kDestroy, kTotals, kPromiseResolve, 55} = internal_async_hooks.constants; 56 57// Listener API // 58 59class AsyncHook { 60 constructor({ init, before, after, destroy, promiseResolve }) { 61 if (init !== undefined && typeof init !== 'function') 62 throw new ERR_ASYNC_CALLBACK('hook.init'); 63 if (before !== undefined && typeof before !== 'function') 64 throw new ERR_ASYNC_CALLBACK('hook.before'); 65 if (after !== undefined && typeof after !== 'function') 66 throw new ERR_ASYNC_CALLBACK('hook.after'); 67 if (destroy !== undefined && typeof destroy !== 'function') 68 throw new ERR_ASYNC_CALLBACK('hook.destroy'); 69 if (promiseResolve !== undefined && typeof promiseResolve !== 'function') 70 throw new ERR_ASYNC_CALLBACK('hook.promiseResolve'); 71 72 this[init_symbol] = init; 73 this[before_symbol] = before; 74 this[after_symbol] = after; 75 this[destroy_symbol] = destroy; 76 this[promise_resolve_symbol] = promiseResolve; 77 } 78 79 enable() { 80 // The set of callbacks for a hook should be the same regardless of whether 81 // enable()/disable() are run during their execution. The following 82 // references are reassigned to the tmp arrays if a hook is currently being 83 // processed. 84 const [hooks_array, hook_fields] = getHookArrays(); 85 86 // Each hook is only allowed to be added once. 87 if (hooks_array.includes(this)) 88 return this; 89 90 const prev_kTotals = hook_fields[kTotals]; 91 92 // createHook() has already enforced that the callbacks are all functions, 93 // so here simply increment the count of whether each callbacks exists or 94 // not. 95 hook_fields[kTotals] = hook_fields[kInit] += +!!this[init_symbol]; 96 hook_fields[kTotals] += hook_fields[kBefore] += +!!this[before_symbol]; 97 hook_fields[kTotals] += hook_fields[kAfter] += +!!this[after_symbol]; 98 hook_fields[kTotals] += hook_fields[kDestroy] += +!!this[destroy_symbol]; 99 hook_fields[kTotals] += 100 hook_fields[kPromiseResolve] += +!!this[promise_resolve_symbol]; 101 hooks_array.push(this); 102 103 if (prev_kTotals === 0 && hook_fields[kTotals] > 0) { 104 enableHooks(); 105 } 106 107 return this; 108 } 109 110 disable() { 111 const [hooks_array, hook_fields] = getHookArrays(); 112 113 const index = hooks_array.indexOf(this); 114 if (index === -1) 115 return this; 116 117 const prev_kTotals = hook_fields[kTotals]; 118 119 hook_fields[kTotals] = hook_fields[kInit] -= +!!this[init_symbol]; 120 hook_fields[kTotals] += hook_fields[kBefore] -= +!!this[before_symbol]; 121 hook_fields[kTotals] += hook_fields[kAfter] -= +!!this[after_symbol]; 122 hook_fields[kTotals] += hook_fields[kDestroy] -= +!!this[destroy_symbol]; 123 hook_fields[kTotals] += 124 hook_fields[kPromiseResolve] -= +!!this[promise_resolve_symbol]; 125 hooks_array.splice(index, 1); 126 127 if (prev_kTotals > 0 && hook_fields[kTotals] === 0) { 128 disableHooks(); 129 } 130 131 return this; 132 } 133} 134 135 136function createHook(fns) { 137 return new AsyncHook(fns); 138} 139 140 141// Embedder API // 142 143const destroyedSymbol = Symbol('destroyed'); 144 145class AsyncResource { 146 constructor(type, opts = {}) { 147 validateString(type, 'type'); 148 149 let triggerAsyncId = opts; 150 let requireManualDestroy = false; 151 if (typeof opts !== 'number') { 152 triggerAsyncId = opts.triggerAsyncId === undefined ? 153 getDefaultTriggerAsyncId() : opts.triggerAsyncId; 154 requireManualDestroy = !!opts.requireManualDestroy; 155 } 156 157 // Unlike emitInitScript, AsyncResource doesn't supports null as the 158 // triggerAsyncId. 159 if (!NumberIsSafeInteger(triggerAsyncId) || triggerAsyncId < -1) { 160 throw new ERR_INVALID_ASYNC_ID('triggerAsyncId', triggerAsyncId); 161 } 162 163 const asyncId = newAsyncId(); 164 this[async_id_symbol] = asyncId; 165 this[trigger_async_id_symbol] = triggerAsyncId; 166 167 if (initHooksExist()) { 168 if (enabledHooksExist() && type.length === 0) { 169 throw new ERR_ASYNC_TYPE(type); 170 } 171 172 emitInit(asyncId, type, triggerAsyncId, this); 173 } 174 175 if (!requireManualDestroy && destroyHooksExist()) { 176 // This prop name (destroyed) has to be synchronized with C++ 177 const destroyed = { destroyed: false }; 178 this[destroyedSymbol] = destroyed; 179 registerDestroyHook(this, asyncId, destroyed); 180 } 181 } 182 183 runInAsyncScope(fn, thisArg, ...args) { 184 const asyncId = this[async_id_symbol]; 185 emitBefore(asyncId, this[trigger_async_id_symbol], this); 186 187 try { 188 const ret = thisArg === undefined ? 189 fn(...args) : 190 ReflectApply(fn, thisArg, args); 191 192 return ret; 193 } finally { 194 if (hasAsyncIdStack()) 195 emitAfter(asyncId); 196 } 197 } 198 199 emitDestroy() { 200 if (this[destroyedSymbol] !== undefined) { 201 this[destroyedSymbol].destroyed = true; 202 } 203 emitDestroy(this[async_id_symbol]); 204 return this; 205 } 206 207 asyncId() { 208 return this[async_id_symbol]; 209 } 210 211 triggerAsyncId() { 212 return this[trigger_async_id_symbol]; 213 } 214 215 bind(fn) { 216 if (typeof fn !== 'function') 217 throw new ERR_INVALID_ARG_TYPE('fn', 'Function', fn); 218 const ret = this.runInAsyncScope.bind(this, fn); 219 ObjectDefineProperties(ret, { 220 'length': { 221 configurable: true, 222 enumerable: false, 223 value: fn.length, 224 writable: false, 225 }, 226 'asyncResource': { 227 configurable: true, 228 enumerable: true, 229 value: this, 230 writable: true, 231 } 232 }); 233 return ret; 234 } 235 236 static bind(fn, type) { 237 type = type || fn.name; 238 return (new AsyncResource(type || 'bound-anonymous-fn')).bind(fn); 239 } 240} 241 242const storageList = []; 243const storageHook = createHook({ 244 init(asyncId, type, triggerAsyncId, resource) { 245 const currentResource = executionAsyncResource(); 246 // Value of currentResource is always a non null object 247 for (let i = 0; i < storageList.length; ++i) { 248 storageList[i]._propagate(resource, currentResource); 249 } 250 } 251}); 252 253const defaultAlsResourceOpts = { requireManualDestroy: true }; 254class AsyncLocalStorage { 255 constructor() { 256 this.kResourceStore = Symbol('kResourceStore'); 257 this.enabled = false; 258 } 259 260 disable() { 261 if (this.enabled) { 262 this.enabled = false; 263 // If this.enabled, the instance must be in storageList 264 storageList.splice(storageList.indexOf(this), 1); 265 if (storageList.length === 0) { 266 storageHook.disable(); 267 } 268 } 269 } 270 271 _enable() { 272 if (!this.enabled) { 273 this.enabled = true; 274 storageList.push(this); 275 storageHook.enable(); 276 } 277 } 278 279 // Propagate the context from a parent resource to a child one 280 _propagate(resource, triggerResource) { 281 const store = triggerResource[this.kResourceStore]; 282 if (this.enabled) { 283 resource[this.kResourceStore] = store; 284 } 285 } 286 287 enterWith(store) { 288 this._enable(); 289 const resource = executionAsyncResource(); 290 resource[this.kResourceStore] = store; 291 } 292 293 run(store, callback, ...args) { 294 // Avoid creation of an AsyncResource if store is already active 295 if (ObjectIs(store, this.getStore())) { 296 return callback(...args); 297 } 298 const resource = new AsyncResource('AsyncLocalStorage', 299 defaultAlsResourceOpts); 300 // Calling emitDestroy before runInAsyncScope avoids a try/finally 301 // It is ok because emitDestroy only schedules calling the hook 302 return resource.emitDestroy().runInAsyncScope(() => { 303 this.enterWith(store); 304 return callback(...args); 305 }); 306 } 307 308 exit(callback, ...args) { 309 if (!this.enabled) { 310 return callback(...args); 311 } 312 this.disable(); 313 try { 314 return callback(...args); 315 } finally { 316 this._enable(); 317 } 318 } 319 320 getStore() { 321 if (this.enabled) { 322 const resource = executionAsyncResource(); 323 return resource[this.kResourceStore]; 324 } 325 } 326} 327 328// Placing all exports down here because the exported classes won't export 329// otherwise. 330module.exports = { 331 // Public API 332 AsyncLocalStorage, 333 createHook, 334 executionAsyncId, 335 triggerAsyncId, 336 executionAsyncResource, 337 // Embedder API 338 AsyncResource, 339}; 340