• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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