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