1'use strict'; 2 3const { 4 ArrayFrom, 5 Boolean, 6 Error, 7 FunctionPrototypeBind, 8 FunctionPrototypeCall, 9 NumberIsInteger, 10 ObjectAssign, 11 ObjectDefineProperties, 12 ObjectDefineProperty, 13 ObjectGetOwnPropertyDescriptor, 14 ReflectApply, 15 SafeMap, 16 String, 17 Symbol, 18 SymbolFor, 19 SymbolToStringTag, 20 SafeWeakSet, 21} = primordials; 22 23const { 24 codes: { 25 ERR_INVALID_ARG_TYPE, 26 ERR_EVENT_RECURSION, 27 ERR_MISSING_ARGS, 28 ERR_INVALID_THIS, 29 } 30} = require('internal/errors'); 31const { validateObject, validateString } = require('internal/validators'); 32 33const { customInspectSymbol } = require('internal/util'); 34const { inspect } = require('util'); 35 36const kIsEventTarget = SymbolFor('nodejs.event_target'); 37 38const EventEmitter = require('events'); 39const { 40 kMaxEventTargetListeners, 41 kMaxEventTargetListenersWarned, 42} = EventEmitter; 43 44const kEvents = Symbol('kEvents'); 45const kStop = Symbol('kStop'); 46const kTarget = Symbol('kTarget'); 47const kHandlers = Symbol('khandlers'); 48 49const kHybridDispatch = SymbolFor('nodejs.internal.kHybridDispatch'); 50const kCreateEvent = Symbol('kCreateEvent'); 51const kNewListener = Symbol('kNewListener'); 52const kRemoveListener = Symbol('kRemoveListener'); 53const kIsNodeStyleListener = Symbol('kIsNodeStyleListener'); 54const kTrustEvent = Symbol('kTrustEvent'); 55 56// Lazy load perf_hooks to avoid the additional overhead on startup 57let perf_hooks; 58function lazyNow() { 59 if (perf_hooks === undefined) 60 perf_hooks = require('perf_hooks'); 61 return perf_hooks.performance.now(); 62} 63 64// TODO(joyeecheung): V8 snapshot does not support instance member 65// initializers for now: 66// https://bugs.chromium.org/p/v8/issues/detail?id=10704 67const kType = Symbol('type'); 68const kDefaultPrevented = Symbol('defaultPrevented'); 69const kCancelable = Symbol('cancelable'); 70const kTimestamp = Symbol('timestamp'); 71const kBubbles = Symbol('bubbles'); 72const kComposed = Symbol('composed'); 73const kPropagationStopped = Symbol('propagationStopped'); 74 75const isTrustedSet = new SafeWeakSet(); 76const isTrusted = ObjectGetOwnPropertyDescriptor({ 77 get isTrusted() { 78 return isTrustedSet.has(this); 79 } 80}, 'isTrusted').get; 81 82class Event { 83 constructor(type, options = null) { 84 if (arguments.length === 0) 85 throw new ERR_MISSING_ARGS('type'); 86 validateObject(options, 'options', { 87 allowArray: true, allowFunction: true, nullable: true, 88 }); 89 const { cancelable, bubbles, composed } = { ...options }; 90 this[kCancelable] = !!cancelable; 91 this[kBubbles] = !!bubbles; 92 this[kComposed] = !!composed; 93 this[kType] = `${type}`; 94 this[kDefaultPrevented] = false; 95 this[kTimestamp] = lazyNow(); 96 this[kPropagationStopped] = false; 97 if (options?.[kTrustEvent]) { 98 isTrustedSet.add(this); 99 } 100 101 // isTrusted is special (LegacyUnforgeable) 102 ObjectDefineProperty(this, 'isTrusted', { 103 get: isTrusted, 104 enumerable: true, 105 configurable: false 106 }); 107 this[kTarget] = null; 108 } 109 110 [customInspectSymbol](depth, options) { 111 const name = this.constructor.name; 112 if (depth < 0) 113 return name; 114 115 const opts = ObjectAssign({}, options, { 116 depth: NumberIsInteger(options.depth) ? options.depth - 1 : options.depth 117 }); 118 119 return `${name} ${inspect({ 120 type: this[kType], 121 defaultPrevented: this[kDefaultPrevented], 122 cancelable: this[kCancelable], 123 timeStamp: this[kTimestamp], 124 }, opts)}`; 125 } 126 127 stopImmediatePropagation() { 128 this[kStop] = true; 129 } 130 131 preventDefault() { 132 this[kDefaultPrevented] = true; 133 } 134 135 get target() { return this[kTarget]; } 136 get currentTarget() { return this[kTarget]; } 137 get srcElement() { return this[kTarget]; } 138 139 get type() { return this[kType]; } 140 141 get cancelable() { return this[kCancelable]; } 142 143 get defaultPrevented() { 144 return this[kCancelable] && this[kDefaultPrevented]; 145 } 146 147 get timeStamp() { return this[kTimestamp]; } 148 149 150 // The following are non-op and unused properties/methods from Web API Event. 151 // These are not supported in Node.js and are provided purely for 152 // API completeness. 153 154 composedPath() { return this[kTarget] ? [this[kTarget]] : []; } 155 get returnValue() { return !this.defaultPrevented; } 156 get bubbles() { return this[kBubbles]; } 157 get composed() { return this[kComposed]; } 158 get eventPhase() { 159 return this[kTarget] ? Event.AT_TARGET : Event.NONE; 160 } 161 get cancelBubble() { return this[kPropagationStopped]; } 162 set cancelBubble(value) { 163 if (value) { 164 this.stopPropagation(); 165 } 166 } 167 stopPropagation() { 168 this[kPropagationStopped] = true; 169 } 170 171 static NONE = 0; 172 static CAPTURING_PHASE = 1; 173 static AT_TARGET = 2; 174 static BUBBLING_PHASE = 3; 175} 176 177ObjectDefineProperty(Event.prototype, SymbolToStringTag, { 178 writable: false, 179 enumerable: false, 180 configurable: true, 181 value: 'Event', 182}); 183 184class NodeCustomEvent extends Event { 185 constructor(type, options) { 186 super(type, options); 187 if (options?.detail) { 188 this.detail = options.detail; 189 } 190 } 191} 192// The listeners for an EventTarget are maintained as a linked list. 193// Unfortunately, the way EventTarget is defined, listeners are accounted 194// using the tuple [handler,capture], and even if we don't actually make 195// use of capture or bubbling, in order to be spec compliant we have to 196// take on the additional complexity of supporting it. Fortunately, using 197// the linked list makes dispatching faster, even if adding/removing is 198// slower. 199class Listener { 200 constructor(previous, listener, once, capture, passive, isNodeStyleListener) { 201 this.next = undefined; 202 if (previous !== undefined) 203 previous.next = this; 204 this.previous = previous; 205 this.listener = listener; 206 this.once = once; 207 this.capture = capture; 208 this.passive = passive; 209 this.isNodeStyleListener = isNodeStyleListener; 210 211 this.callback = 212 typeof listener === 'function' ? 213 listener : 214 FunctionPrototypeBind(listener.handleEvent, listener); 215 } 216 217 same(listener, capture) { 218 return this.listener === listener && this.capture === capture; 219 } 220 221 remove() { 222 if (this.previous !== undefined) 223 this.previous.next = this.next; 224 if (this.next !== undefined) 225 this.next.previous = this.previous; 226 } 227} 228 229function initEventTarget(self) { 230 self[kEvents] = new SafeMap(); 231 self[kMaxEventTargetListeners] = EventEmitter.defaultMaxListeners; 232 self[kMaxEventTargetListenersWarned] = false; 233} 234 235class EventTarget { 236 // Used in checking whether an object is an EventTarget. This is a well-known 237 // symbol as EventTarget may be used cross-realm. 238 // Ref: https://github.com/nodejs/node/pull/33661 239 static [kIsEventTarget] = true; 240 241 constructor() { 242 initEventTarget(this); 243 } 244 245 [kNewListener](size, type, listener, once, capture, passive) { 246 if (this[kMaxEventTargetListeners] > 0 && 247 size > this[kMaxEventTargetListeners] && 248 !this[kMaxEventTargetListenersWarned]) { 249 this[kMaxEventTargetListenersWarned] = true; 250 // No error code for this since it is a Warning 251 // eslint-disable-next-line no-restricted-syntax 252 const w = new Error('Possible EventTarget memory leak detected. ' + 253 `${size} ${type} listeners ` + 254 `added to ${inspect(this, { depth: -1 })}. Use ` + 255 'events.setMaxListeners() to increase limit'); 256 w.name = 'MaxListenersExceededWarning'; 257 w.target = this; 258 w.type = type; 259 w.count = size; 260 process.emitWarning(w); 261 } 262 } 263 [kRemoveListener](size, type, listener, capture) {} 264 265 addEventListener(type, listener, options = {}) { 266 if (arguments.length < 2) 267 throw new ERR_MISSING_ARGS('type', 'listener'); 268 269 // We validateOptions before the shouldAddListeners check because the spec 270 // requires us to hit getters. 271 const { 272 once, 273 capture, 274 passive, 275 isNodeStyleListener 276 } = validateEventListenerOptions(options); 277 278 if (!shouldAddListener(listener)) { 279 // The DOM silently allows passing undefined as a second argument 280 // No error code for this since it is a Warning 281 // eslint-disable-next-line no-restricted-syntax 282 const w = new Error(`addEventListener called with ${listener}` + 283 ' which has no effect.'); 284 w.name = 'AddEventListenerArgumentTypeWarning'; 285 w.target = this; 286 w.type = type; 287 process.emitWarning(w); 288 return; 289 } 290 type = String(type); 291 292 let root = this[kEvents].get(type); 293 294 if (root === undefined) { 295 root = { size: 1, next: undefined }; 296 // This is the first handler in our linked list. 297 new Listener(root, listener, once, capture, passive, isNodeStyleListener); 298 this[kNewListener](root.size, type, listener, once, capture, passive); 299 this[kEvents].set(type, root); 300 return; 301 } 302 303 let handler = root.next; 304 let previous = root; 305 306 // We have to walk the linked list to see if we have a match 307 while (handler !== undefined && !handler.same(listener, capture)) { 308 previous = handler; 309 handler = handler.next; 310 } 311 312 if (handler !== undefined) { // Duplicate! Ignore 313 return; 314 } 315 316 new Listener(previous, listener, once, capture, passive, 317 isNodeStyleListener); 318 root.size++; 319 this[kNewListener](root.size, type, listener, once, capture, passive); 320 } 321 322 removeEventListener(type, listener, options = {}) { 323 if (!shouldAddListener(listener)) 324 return; 325 326 type = String(type); 327 const capture = options?.capture === true; 328 329 const root = this[kEvents].get(type); 330 if (root === undefined || root.next === undefined) 331 return; 332 333 let handler = root.next; 334 while (handler !== undefined) { 335 if (handler.same(listener, capture)) { 336 handler.remove(); 337 root.size--; 338 if (root.size === 0) 339 this[kEvents].delete(type); 340 this[kRemoveListener](root.size, type, listener, capture); 341 break; 342 } 343 handler = handler.next; 344 } 345 } 346 347 dispatchEvent(event) { 348 if (!(event instanceof Event)) 349 throw new ERR_INVALID_ARG_TYPE('event', 'Event', event); 350 351 if (!isEventTarget(this)) 352 throw new ERR_INVALID_THIS('EventTarget'); 353 354 if (event[kTarget] !== null) 355 throw new ERR_EVENT_RECURSION(event.type); 356 357 this[kHybridDispatch](event, event.type, event); 358 359 return event.defaultPrevented !== true; 360 } 361 362 [kHybridDispatch](nodeValue, type, event) { 363 const createEvent = () => { 364 if (event === undefined) { 365 event = this[kCreateEvent](nodeValue, type); 366 event[kTarget] = this; 367 } 368 return event; 369 }; 370 if (event !== undefined) 371 event[kTarget] = this; 372 373 const root = this[kEvents].get(type); 374 if (root === undefined || root.next === undefined) 375 return true; 376 377 let handler = root.next; 378 let next; 379 380 while (handler !== undefined && 381 (handler.passive || event?.[kStop] !== true)) { 382 // Cache the next item in case this iteration removes the current one 383 next = handler.next; 384 385 if (handler.once) { 386 handler.remove(); 387 root.size--; 388 const { listener, capture } = handler; 389 this[kRemoveListener](root.size, type, listener, capture); 390 } 391 392 try { 393 let arg; 394 if (handler.isNodeStyleListener) { 395 arg = nodeValue; 396 } else { 397 arg = createEvent(); 398 } 399 const result = FunctionPrototypeCall(handler.callback, this, arg); 400 if (result !== undefined && result !== null) 401 addCatch(this, result, createEvent()); 402 } catch (err) { 403 emitUnhandledRejectionOrErr(this, err, createEvent()); 404 } 405 406 handler = next; 407 } 408 409 if (event !== undefined) 410 event[kTarget] = undefined; 411 } 412 413 [kCreateEvent](nodeValue, type) { 414 return new NodeCustomEvent(type, { detail: nodeValue }); 415 } 416 [customInspectSymbol](depth, options) { 417 const name = this.constructor.name; 418 if (depth < 0) 419 return name; 420 421 const opts = ObjectAssign({}, options, { 422 depth: NumberIsInteger(options.depth) ? options.depth - 1 : options.depth 423 }); 424 425 return `${name} ${inspect({}, opts)}`; 426 } 427} 428 429ObjectDefineProperties(EventTarget.prototype, { 430 addEventListener: { enumerable: true }, 431 removeEventListener: { enumerable: true }, 432 dispatchEvent: { enumerable: true } 433}); 434ObjectDefineProperty(EventTarget.prototype, SymbolToStringTag, { 435 writable: false, 436 enumerable: false, 437 configurable: true, 438 value: 'EventTarget', 439}); 440 441function initNodeEventTarget(self) { 442 initEventTarget(self); 443} 444 445class NodeEventTarget extends EventTarget { 446 static defaultMaxListeners = 10; 447 448 constructor() { 449 super(); 450 initNodeEventTarget(this); 451 } 452 453 setMaxListeners(n) { 454 EventEmitter.setMaxListeners(n, this); 455 } 456 457 getMaxListeners() { 458 return this[kMaxEventTargetListeners]; 459 } 460 461 eventNames() { 462 return ArrayFrom(this[kEvents].keys()); 463 } 464 465 listenerCount(type) { 466 const root = this[kEvents].get(String(type)); 467 return root !== undefined ? root.size : 0; 468 } 469 470 off(type, listener, options) { 471 this.removeEventListener(type, listener, options); 472 return this; 473 } 474 475 removeListener(type, listener, options) { 476 this.removeEventListener(type, listener, options); 477 return this; 478 } 479 480 on(type, listener) { 481 this.addEventListener(type, listener, { [kIsNodeStyleListener]: true }); 482 return this; 483 } 484 485 addListener(type, listener) { 486 this.addEventListener(type, listener, { [kIsNodeStyleListener]: true }); 487 return this; 488 } 489 emit(type, arg) { 490 validateString(type, 'type'); 491 const hadListeners = this.listenerCount(type) > 0; 492 this[kHybridDispatch](arg, type); 493 return hadListeners; 494 } 495 496 once(type, listener) { 497 this.addEventListener(type, listener, 498 { once: true, [kIsNodeStyleListener]: true }); 499 return this; 500 } 501 502 removeAllListeners(type) { 503 if (type !== undefined) { 504 this[kEvents].delete(String(type)); 505 } else { 506 this[kEvents].clear(); 507 } 508 509 return this; 510 } 511} 512 513ObjectDefineProperties(NodeEventTarget.prototype, { 514 setMaxListeners: { enumerable: true }, 515 getMaxListeners: { enumerable: true }, 516 eventNames: { enumerable: true }, 517 listenerCount: { enumerable: true }, 518 off: { enumerable: true }, 519 removeListener: { enumerable: true }, 520 on: { enumerable: true }, 521 addListener: { enumerable: true }, 522 once: { enumerable: true }, 523 emit: { enumerable: true }, 524 removeAllListeners: { enumerable: true }, 525}); 526 527// EventTarget API 528 529function shouldAddListener(listener) { 530 if (typeof listener === 'function' || 531 typeof listener?.handleEvent === 'function') { 532 return true; 533 } 534 535 if (listener == null) 536 return false; 537 538 throw new ERR_INVALID_ARG_TYPE('listener', 'EventListener', listener); 539} 540 541function validateEventListenerOptions(options) { 542 if (typeof options === 'boolean') 543 return { capture: options }; 544 545 if (options === null) 546 return {}; 547 validateObject(options, 'options', { 548 allowArray: true, allowFunction: true, 549 }); 550 return { 551 once: Boolean(options.once), 552 capture: Boolean(options.capture), 553 passive: Boolean(options.passive), 554 isNodeStyleListener: Boolean(options[kIsNodeStyleListener]) 555 }; 556} 557 558// Test whether the argument is an event object. This is far from a fool-proof 559// test, for example this input will result in a false positive: 560// > isEventTarget({ constructor: EventTarget }) 561// It stands in its current implementation as a compromise. 562// Ref: https://github.com/nodejs/node/pull/33661 563function isEventTarget(obj) { 564 return obj?.constructor?.[kIsEventTarget]; 565} 566 567function addCatch(that, promise, event) { 568 const then = promise.then; 569 if (typeof then === 'function') { 570 FunctionPrototypeCall(then, promise, undefined, function(err) { 571 // The callback is called with nextTick to avoid a follow-up 572 // rejection from this promise. 573 process.nextTick(emitUnhandledRejectionOrErr, that, err, event); 574 }); 575 } 576} 577 578function emitUnhandledRejectionOrErr(that, err, event) { 579 process.emit('error', err, event); 580} 581 582function makeEventHandler(handler) { 583 // Event handlers are dispatched in the order they were first set 584 // See https://github.com/nodejs/node/pull/35949#issuecomment-722496598 585 function eventHandler(...args) { 586 if (typeof eventHandler.handler !== 'function') { 587 return; 588 } 589 return ReflectApply(eventHandler.handler, this, args); 590 } 591 eventHandler.handler = handler; 592 return eventHandler; 593} 594 595function defineEventHandler(emitter, name) { 596 // 8.1.5.1 Event handlers - basically `on[eventName]` attributes 597 ObjectDefineProperty(emitter, `on${name}`, { 598 get() { 599 return this[kHandlers]?.get(name)?.handler; 600 }, 601 set(value) { 602 if (!this[kHandlers]) { 603 this[kHandlers] = new SafeMap(); 604 } 605 let wrappedHandler = this[kHandlers]?.get(name); 606 if (wrappedHandler) { 607 if (typeof wrappedHandler.handler === 'function') { 608 this[kEvents].get(name).size--; 609 const size = this[kEvents].get(name).size; 610 this[kRemoveListener](size, name, wrappedHandler.handler, false); 611 } 612 wrappedHandler.handler = value; 613 if (typeof wrappedHandler.handler === 'function') { 614 this[kEvents].get(name).size++; 615 const size = this[kEvents].get(name).size; 616 this[kNewListener](size, name, value, false, false, false); 617 } 618 } else { 619 wrappedHandler = makeEventHandler(value); 620 this.addEventListener(name, wrappedHandler); 621 } 622 this[kHandlers].set(name, wrappedHandler); 623 }, 624 configurable: true, 625 enumerable: true 626 }); 627} 628module.exports = { 629 Event, 630 EventTarget, 631 NodeEventTarget, 632 defineEventHandler, 633 initEventTarget, 634 initNodeEventTarget, 635 kCreateEvent, 636 kNewListener, 637 kTrustEvent, 638 kRemoveListener, 639 kEvents, 640 isEventTarget, 641}; 642