Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/nodejs/node.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJames M Snell <jasnell@gmail.com>2020-05-24 01:43:58 +0300
committerJames M Snell <jasnell@gmail.com>2020-05-28 16:31:58 +0300
commit785842a053d62820027c6b62b41110113a7f1755 (patch)
tree0604009d2b0a263402add3718d2580127a105275 /lib/internal/event_target.js
parent47044a91c6269a395098cf74d4b2a2e70df5c321 (diff)
events: initial implementation of experimental EventTarget
See documentation changes for details Signed-off-by: James M Snell <jasnell@gmail.com> PR-URL: https://github.com/nodejs/node/pull/33556 Refs: https://github.com/nodejs/node/pull/33527 Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com> Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: Bradley Farias <bradley.meck@gmail.com>
Diffstat (limited to 'lib/internal/event_target.js')
-rw-r--r--lib/internal/event_target.js434
1 files changed, 434 insertions, 0 deletions
diff --git a/lib/internal/event_target.js b/lib/internal/event_target.js
new file mode 100644
index 00000000000..4cc561f95c0
--- /dev/null
+++ b/lib/internal/event_target.js
@@ -0,0 +1,434 @@
+'use strict';
+
+const {
+ ArrayFrom,
+ Error,
+ Map,
+ Object,
+ Set,
+ Symbol,
+ NumberIsNaN,
+} = primordials;
+
+const {
+ codes: {
+ ERR_INVALID_ARG_TYPE,
+ ERR_EVENT_RECURSION,
+ ERR_OUT_OF_RANGE,
+ }
+} = require('internal/errors');
+
+const perf_hooks = require('perf_hooks');
+const { customInspectSymbol } = require('internal/util');
+const { inspect } = require('util');
+
+const kEvents = Symbol('kEvents');
+const kStop = Symbol('kStop');
+const kTarget = Symbol('kTarget');
+
+const kNewListener = Symbol('kNewListener');
+const kRemoveListener = Symbol('kRemoveListener');
+
+class Event {
+ #type = undefined;
+ #defaultPrevented = false;
+ #cancelable = false;
+ #timestamp = perf_hooks.performance.now();
+
+ // Neither of these are currently used in the Node.js implementation
+ // of EventTarget because there is no concept of bubbling or
+ // composition. We preserve their values in Event but they are
+ // non-ops and do not carry any semantics in Node.js
+ #bubbles = false;
+ #composed = false;
+
+
+ constructor(type, options) {
+ if (options != null && typeof options !== 'object')
+ throw new ERR_INVALID_ARG_TYPE('options', 'object', options);
+ const { cancelable, bubbles, composed } = { ...options };
+ this.#cancelable = !!cancelable;
+ this.#bubbles = !!bubbles;
+ this.#composed = !!composed;
+ this.#type = String(type);
+ }
+
+ [customInspectSymbol](depth, options) {
+ const name = this.constructor.name;
+ if (depth < 0)
+ return name;
+
+ const opts = Object.assign({}, options, {
+ dept: options.depth === null ? null : options.depth - 1
+ });
+
+ return `${name} ${inspect({
+ type: this.#type,
+ defaultPrevented: this.#defaultPrevented,
+ cancelable: this.#cancelable,
+ timeStamp: this.#timestamp,
+ }, opts)}`;
+ }
+
+ stopImmediatePropagation() {
+ this[kStop] = true;
+ }
+
+ preventDefault() {
+ this.#defaultPrevented = true;
+ }
+
+ get target() { return this[kTarget]; }
+ get currentTarget() { return this[kTarget]; }
+ get srcElement() { return this[kTarget]; }
+
+ get type() { return this.#type; }
+
+ get cancelable() { return this.#cancelable; }
+
+ get defaultPrevented() { return this.#cancelable && this.#defaultPrevented; }
+
+ get timeStamp() { return this.#timestamp; }
+
+
+ // The following are non-op and unused properties/methods from Web API Event.
+ // These are not supported in Node.js and are provided purely for
+ // API completeness.
+
+ composedPath() { return this[kTarget] ? [this[kTarget]] : []; }
+ get returnValue() { return !this.defaultPrevented; }
+ get bubbles() { return this.#bubbles; }
+ get composed() { return this.#composed; }
+ get isTrusted() { return false; }
+ get eventPhase() {
+ return this[kTarget] ? 2 : 0; // Equivalent to AT_TARGET or NONE
+ }
+ cancelBubble() {
+ // Non-op in Node.js. Alias for stopPropagation
+ }
+ stopPropagation() {
+ // Non-op in Node.js
+ }
+
+ get [Symbol.toStringTag]() { return 'Event'; }
+}
+
+// The listeners for an EventTarget are maintained as a linked list.
+// Unfortunately, the way EventTarget is defined, listeners are accounted
+// using the tuple [handler,capture], and even if we don't actually make
+// use of capture or bubbling, in order to be spec compliant we have to
+// take on the additional complexity of supporting it. Fortunately, using
+// the linked list makes dispatching faster, even if adding/removing is
+// slower.
+class Listener {
+ next;
+ previous;
+ listener;
+ callback;
+ once;
+ capture;
+ passive;
+
+ constructor(previous, listener, once, capture, passive) {
+ if (previous !== undefined)
+ previous.next = this;
+ this.previous = previous;
+ this.listener = listener;
+ this.once = once;
+ this.capture = capture;
+ this.passive = passive;
+
+ this.callback =
+ typeof listener === 'function' ?
+ listener :
+ listener.handleEvent.bind(listener);
+ }
+
+ same(listener, capture) {
+ return this.listener === listener && this.capture === capture;
+ }
+
+ remove() {
+ if (this.previous !== undefined)
+ this.previous.next = this.next;
+ if (this.next !== undefined)
+ this.next.previous = this.previous;
+ }
+}
+
+class EventTarget {
+ [kEvents] = new Map();
+ #emitting = new Set();
+
+ [kNewListener](size, type, listener, once, capture, passive) {}
+ [kRemoveListener](size, type, listener, capture) {}
+
+ addEventListener(type, listener, options = {}) {
+ validateListener(listener);
+ type = String(type);
+
+ const {
+ once,
+ capture,
+ passive
+ } = validateEventListenerOptions(options);
+
+ let root = this[kEvents].get(type);
+
+ if (root === undefined) {
+ root = { size: 1, next: undefined };
+ // This is the first handler in our linked list.
+ new Listener(root, listener, once, capture, passive);
+ this[kNewListener](root.size, type, listener, once, capture, passive);
+ this[kEvents].set(type, root);
+ return;
+ }
+
+ let handler = root.next;
+ let previous;
+
+ // We have to walk the linked list to see if we have a match
+ while (handler !== undefined && !handler.same(listener, capture)) {
+ previous = handler;
+ handler = handler.next;
+ }
+
+ if (handler !== undefined) { // Duplicate! Ignore
+ return;
+ }
+
+ new Listener(previous, listener, once, capture, passive);
+ root.size++;
+ this[kNewListener](root.size, type, listener, once, capture, passive);
+ }
+
+ removeEventListener(type, listener, options = {}) {
+ validateListener(listener);
+ type = String(type);
+ const { capture } = validateEventListenerOptions(options);
+ const root = this[kEvents].get(type);
+ if (root === undefined || root.next === undefined)
+ return;
+
+ let handler = root.next;
+ while (handler !== undefined) {
+ if (handler.same(listener, capture)) {
+ handler.remove();
+ root.size--;
+ if (root.size === 0)
+ this[kEvents].delete(type);
+ this[kRemoveListener](root.size, type, listener, capture);
+ break;
+ }
+ handler = handler.next;
+ }
+ }
+
+ dispatchEvent(event) {
+ if (!(event instanceof Event)) {
+ throw new ERR_INVALID_ARG_TYPE('event', 'Event', event);
+ }
+
+ if (this.#emitting.has(event.type) ||
+ event[kTarget] !== undefined) {
+ throw new ERR_EVENT_RECURSION(event.type);
+ }
+
+ const root = this[kEvents].get(event.type);
+ if (root === undefined || root.next === undefined)
+ return true;
+
+ event[kTarget] = this;
+ this.#emitting.add(event.type);
+
+ let handler = root.next;
+ let next;
+
+ while (handler !== undefined &&
+ (handler.passive || event[kStop] !== true)) {
+ // Cache the next item in case this iteration removes the current one
+ next = handler.next;
+
+ if (handler.once) {
+ handler.remove();
+ root.size--;
+ }
+
+ try {
+ const result = handler.callback.call(this, event);
+ if (result !== undefined && result !== null)
+ addCatch(this, result, event);
+ } catch (err) {
+ emitUnhandledRejectionOrErr(this, err, event);
+ }
+
+ handler = next;
+ }
+
+ this.#emitting.delete(event.type);
+ event[kTarget] = undefined;
+
+ return event.defaultPrevented === true ? false : true;
+ }
+
+ [customInspectSymbol](depth, options) {
+ const name = this.constructor.name;
+ if (depth < 0)
+ return name;
+
+ const opts = Object.assign({}, options, {
+ dept: options.depth === null ? null : options.depth - 1
+ });
+
+ return `${name} ${inspect({}, opts)}`;
+ }
+}
+
+Object.defineProperties(EventTarget.prototype, {
+ addEventListener: { enumerable: true },
+ removeEventListener: { enumerable: true },
+ dispatchEvent: { enumerable: true }
+});
+
+class NodeEventTarget extends EventTarget {
+ static defaultMaxListeners = 10;
+
+ #maxListeners = NodeEventTarget.defaultMaxListeners;
+ #maxListenersWarned = false;
+
+ [kNewListener](size, type, listener, once, capture, passive) {
+ if (this.#maxListeners > 0 &&
+ size > this.#maxListeners &&
+ !this.#maxListenersWarned) {
+ this.#maxListenersWarned = true;
+ // No error code for this since it is a Warning
+ // eslint-disable-next-line no-restricted-syntax
+ const w = new Error('Possible EventTarget memory leak detected. ' +
+ `${size} ${type} listeners ` +
+ `added to ${inspect(this, { depth: -1 })}. Use ` +
+ 'setMaxListeners() to increase limit');
+ w.name = 'MaxListenersExceededWarning';
+ w.target = this;
+ w.type = type;
+ w.count = size;
+ process.emitWarning(w);
+ }
+ }
+
+ setMaxListeners(n) {
+ if (typeof n !== 'number' || n < 0 || NumberIsNaN(n)) {
+ throw new ERR_OUT_OF_RANGE('n', 'a non-negative number', n);
+ }
+ this.#maxListeners = n;
+ return this;
+ }
+
+ getMaxListeners() {
+ return this.#maxListeners;
+ }
+
+ eventNames() {
+ return ArrayFrom(this[kEvents].keys());
+ }
+
+ listenerCount(type) {
+ const root = this[kEvents].get(String(type));
+ return root !== undefined ? root.size : 0;
+ }
+
+ off(type, listener, options) {
+ this.removeEventListener(type, listener, options);
+ return this;
+ }
+
+ removeListener(type, listener, options) {
+ this.removeEventListener(type, listener, options);
+ return this;
+ }
+
+ on(type, listener) {
+ this.addEventListener(type, listener);
+ return this;
+ }
+
+ addListener(type, listener) {
+ this.addEventListener(type, listener);
+ return this;
+ }
+
+ once(type, listener) {
+ this.addEventListener(type, listener, { once: true });
+ return this;
+ }
+
+ removeAllListeners(type) {
+ if (type !== undefined) {
+ this[kEvents].delete(String(type));
+ } else {
+ this[kEvents].clear();
+ }
+ }
+}
+
+Object.defineProperties(NodeEventTarget.prototype, {
+ setMaxListeners: { enumerable: true },
+ getMaxListeners: { enumerable: true },
+ eventNames: { enumerable: true },
+ listenerCount: { enumerable: true },
+ off: { enumerable: true },
+ removeListener: { enumerable: true },
+ on: { enumerable: true },
+ addListener: { enumerable: true },
+ once: { enumerable: true },
+ removeAllListeners: { enumerable: true },
+});
+
+// EventTarget API
+
+function validateListener(listener) {
+ if (typeof listener === 'function' ||
+ (listener != null &&
+ typeof listener === 'object' &&
+ typeof listener.handleEvent === 'function')) {
+ return;
+ }
+ throw new ERR_INVALID_ARG_TYPE('listener', 'EventListener', listener);
+}
+
+function validateEventListenerOptions(options) {
+ if (options == null || typeof options !== 'object')
+ throw new ERR_INVALID_ARG_TYPE('options', 'object', options);
+ const {
+ once = false,
+ capture = false,
+ passive = false,
+ } = options;
+ return {
+ once: !!once,
+ capture: !!capture,
+ passive: !!passive,
+ };
+}
+
+function addCatch(that, promise, event) {
+ const then = promise.then;
+ if (typeof then === 'function') {
+ then.call(promise, undefined, function(err) {
+ // The callback is called with nextTick to avoid a follow-up
+ // rejection from this promise.
+ process.nextTick(emitUnhandledRejectionOrErr, that, err, event);
+ });
+ }
+}
+
+function emitUnhandledRejectionOrErr(that, err, event) {
+ process.emit('error', err, event);
+}
+
+// EventEmitter-ish API:
+
+module.exports = {
+ Event,
+ EventTarget,
+ NodeEventTarget,
+};