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