profile
viewpoint
James M Snell jasnell @nearform Clovis, California http://jasnell.me One who writes code. #notmypresident

jasnell/activitystrea.ms 139

Activity Streams 2.0 for Node.js (this package is not actively maintained, if you'd like to help, let me know)

Fishrock123/bob 66

🚰 binary data "streams+" via data producers, data consumers, and pull flow.

davidmarkclements/proffer 11

Realtime V8 Tick Profiler

addaleax/node 5

Node.js JavaScript runtime :sparkles::turtle::rocket::sparkles:

jasnell/activitystreams 5

Activity Streams 2.0 Java Reference Implementation

jasnell/activitystreams.jsonld 5

http://asjsonld.mybluemix.net

jasnell/as2-schema 4

activitystrea.ms schema.org extensions

pull request commentnodejs/node

assert: improve assert call tracker

@BridgeAR ... Looking this over again it definitely has good things but i would still prefer that it retain the CallTracker object and build more on the work that Conor did rather than completely undoing that work. I'll work up some additional feedback tomorrow

BridgeAR

comment created time in an hour

pull request commentnodejs/TSC

Proposal: Node.js Contributor Survey

The idea of doing a contributor survey was raised at either the last TSC meeting or the one before (I can't recall exactly). There were no objections so I am moving forward with this. I am currently putting the survey draft together on survey monkey and will post links to the survey once it is ready to go. As there have been no further suggestions on questions, I'll be going with what has been discussed already unless there are objections

jasnell

comment created time in 5 hours

push eventnodejs/TSC

James M Snell

commit sha ced06eb91612bfe4d0db05d86280393cbdbe1814

proposal: Node.js Contributor Survey Building on the discussion that has been started in https://github.com/nodejs/TSC/issues/864, I'd like to propose that we take a queue from other significant open source projects and conduct our own Contributor Survey -- with the intent of helping us to determine where we can continue to make improvements to our contribution process. Using the 2019 Kubernetes Contributor Survey as an initial starting point, I've put together an initial set of questions that can allow us to start structuring something.

view details

push time in 5 hours

created tagpiscinajs/piscina

tagv1.6.1

A fast, efficient Node.js Worker Thread Pool implementation

created time in 6 hours

delete branch piscinajs/piscina

delete branch : bump-to-1.6.1

delete time in 6 hours

push eventpiscinajs/piscina

James M Snell

commit sha 710c9d693c486f1cca64d86c26103f7a825b51bf

Bump to 1.6.1 (#78)

view details

push time in 6 hours

PR merged piscinajs/piscina

Bump to 1.6.1
+6 -1

0 comment

2 changed files

jasnell

pr closed time in 6 hours

push eventpiscinajs/piscina

James M Snell

commit sha 8cf0c4b444a01e33252e4154b8734ebccc39ce25

Update README.md

view details

push time in 6 hours

Pull request review commentpiscinajs/piscina

Bump to 1.6.1

 as a configuration option in lieu of always creating their own.  ## Release Notes +### 1.6.1++* Bug fix: Reject is AbortSignal is already aborted
* Bug fix: Reject if AbortSignal is already aborted
jasnell

comment created time in 6 hours

PR opened piscinajs/piscina

Bump to 1.6.1
+6 -1

0 comment

2 changed files

pr created time in 6 hours

create barnchpiscinajs/piscina

branch : bump-to-1.6.1

created branch time in 6 hours

push eventjasnell/node

James M Snell

commit sha deb200cac0cc35abec7ba1a3fa87e6b2c86e2d3a

fixup! events: initial implementation of experimental EventTarget

view details

push time in 6 hours

delete branch piscinajs/piscina

delete branch : abort-once-only

delete time in 6 hours

push eventpiscinajs/piscina

James M Snell

commit sha 38f2c24cd32327d0d5b5b73f72e31b085275abdb

Use once listener for abort event (#77) Co-authored-by: Anna Henningsen <anna@addaleax.net>

view details

push time in 6 hours

PR merged piscinajs/piscina

Use once listener for abort event

There's no reason to keep the listener around.

+11 -4

0 comment

1 changed file

jasnell

pr closed time in 6 hours

push eventpiscinajs/piscina

James M Snell

commit sha 5517996e214aba2071161d0e28475efd4b893446

Update src/index.ts Co-authored-by: Anna Henningsen <anna@addaleax.net>

view details

push time in 6 hours

pull request commentnodejs/node

lib: initial experimental AbortController implementation

@benjamingr ... quick note... this is currently extending from NodeEventTarget ... in this, however, the remove listener functions are not used and the only ones we definitely need here are the on/once/addListener in order to achieve the use cases we need for AbortSignal

jasnell

comment created time in 7 hours

push eventjasnell/node

James M Snell

commit sha ab56f58b934e8406d7d95ff30cf94513d951ffd8

events: initial implementation of experimental EventTarget See documentation changes for details Signed-off-by: James M Snell <jasnell@gmail.com>

view details

James M Snell

commit sha 98d96d5b6bd1ff9e66fdfa8702a857e1c2eb2350

lib: initial experimental AbortController implementation AbortController impl based very closely on: https://github.com/mysticatea/abort-controller Marked experimental. Not currently used by any of the existing promise apis. Signed-off-by: James M Snell <jasnell@gmail.com>

view details

push time in 7 hours

push eventjasnell/node

James M Snell

commit sha df25184e0305c57b1a964e4e0c26980434362cd7

events: initial implementation of experimental EventTarget See documentation changes for details Signed-off-by: James M Snell <jasnell@gmail.com>

view details

push time in 7 hours

push eventjasnell/node

James M Snell

commit sha 95b6541669e5f847eb9f725ae632998254776adf

fixup! lib: initial experimental AbortController implementation

view details

push time in 7 hours

pull request commentnodejs/node

events: initial implementation of experimental EventTarget

This is passing CI and is otherwise author ready, just needs a few days to sit to land.

jasnell

comment created time in 7 hours

pull request commentnodejs/node

lib: initial experimental AbortController implementation

Updated based on the #33556

jasnell

comment created time in 7 hours

push eventjasnell/node

James M Snell

commit sha 7290192c80e310cf5f9d0ca1ebdba38742926261

lib: initial experimental AbortController implementation AbortController impl based very closely on: https://github.com/mysticatea/abort-controller Marked experimental. Not currently used by any of the existing promise apis.

view details

push time in 7 hours

pull request commentnodejs/node

events: initial implementation of experimental EventTarget

A flag definitely would serve no purpose here.

jasnell

comment created time in 7 hours

issue commentnodejs/TSC

Node.js Technical Steering Committee (TSC) Meeting 2020-05-27

If it's the only thing on the agenda I'm good with skipping this one.

mhdawson

comment created time in 9 hours

push eventjasnell/node

James M Snell

commit sha c9d717f24031ae239216a02a9941b78edd7b6768

events: initial implementation of experimental EventTarget See documentation changes for details Signed-off-by: James M Snell <jasnell@gmail.com>

view details

James M Snell

commit sha b8ceec747a425980f7c5f1b8b7646dcd131a3e0d

fixup! events: initial implementation of experimental EventTarget

view details

James M Snell

commit sha 7e426671eead3651295bbb0131c78f2db6b37052

fixup! fixup! events: initial implementation of experimental EventTarget

view details

James M Snell

commit sha 02953f1cece47899f0d66dd65f80199a3873359d

lib: initial experimental AbortController implementation AbortController impl based very closely on: https://github.com/mysticatea/abort-controller Marked experimental. Not currently used by any of the existing promise apis.

view details

push time in 9 hours

PR opened piscinajs/piscina

Use once listener for abort event

There's no reason to keep the listener around.

+11 -3

0 comment

1 changed file

pr created time in 10 hours

create barnchpiscinajs/piscina

branch : abort-once-only

created branch time in 10 hours

push eventjasnell/node

James M Snell

commit sha 7e426671eead3651295bbb0131c78f2db6b37052

fixup! fixup! events: initial implementation of experimental EventTarget

view details

push time in 10 hours

push eventjasnell/node

James M Snell

commit sha 6d263c7df143b7e64f293ad035bce966d059a362

fixup! fixup! events: initial implementation of experimental EventTarget

view details

push time in 10 hours

push eventjasnell/node

James M Snell

commit sha b8ceec747a425980f7c5f1b8b7646dcd131a3e0d

fixup! events: initial implementation of experimental EventTarget

view details

push time in 10 hours

pull request commentopenssl/openssl

WIP: master QUIC support

Fwiw, rebasing on 1.1.1g was painless and required no changes

tmshort

comment created time in 10 hours

pull request commentnodejs/node

events: initial implementation of experimental EventTarget

Although we are not yet exposing these to public API, this is significant enough that I marked it semver-minor.

jasnell

comment created time in 11 hours

pull request commentnodejs/node

events: initial implementation of experimental EventTarget

Ok, the implementation here has been updated quite a bit to help close the gap with the standard definition.

  • The various options on Event are captured even tho they are not used.
  • The capture and passive options for adding/removing event handlers are added and the capture event is now used as part of the record keeping for listeners.
  • dispatchEvent() performance is significantly improved with a 5x-6x improvement over previous... it's still a whole lot slower than EventEmitter but it's a lot better than it was.

A number of other improvements and tweaks were made that close the gap on the standards API. The one major variance from the standard remaining is the support for passing in { type: 'foo'} pseudo-Events.

jasnell

comment created time in 11 hours

push eventjasnell/node

James M Snell

commit sha c9d717f24031ae239216a02a9941b78edd7b6768

events: initial implementation of experimental EventTarget See documentation changes for details Signed-off-by: James M Snell <jasnell@gmail.com>

view details

push time in 11 hours

pull request commentnodejs/node

events: initial implementation of experimental EventTarget

Indeed. The approach here was to start with the absolute minimum we need to function so that we could work out the semantics and minimal requirements that are specific to Node.js then iterate out from there. Now that we hopefully have most of the interop concerns addressed, I'm hoping we can steer back to some of the other bits so we can make sure those are correct.

jasnell

comment created time in 17 hours

issue commentnodejs/node

EventTarget in Node - should it expose capabilities like removeAllListeners?

Ah! I was mistaken about the abort-controller module! There was an impl that I came across that did support both but for the life of me I can't remember the name of it and for some reason had it in my mind that it was abort-controller!

The remove functions in event emitter are absolutely question marks either way. The only ones that I absolutely think we need to have are on/once/addListener. Everything else is optional.

benjamingr

comment created time in 17 hours

issue commentnodejs/node

EventTarget in Node - should it expose capabilities like removeAllListeners?

The plan for AbortController.signal is that it would be a NodeEventTarget in order to preserve ecosystem compatibility (there are existing AbortController implementations whose signal implements subsets of both EventTarget and EventEmitter)

benjamingr

comment created time in a day

pull request commentnodejs/node

events: initial implementation of experimental EventTarget

Remaining variances from Web API:

  • The bubbles and composed Event options are ignored. (in discussion)
  • removeEventListener ignores options (will be updating)
  • { type: 'foo'} style pseudo-event objects still supported (in discussion)
  • No concept of bubbling or composition. (no plans to implement, relevant properties are non-ops)
jasnell

comment created time in a day

push eventjasnell/node

James M Snell

commit sha 510d08f67a7e051bfbea3638f92943a38951c395

[Squash] stopPropagation non-op

view details

push time in a day

pull request commentnodejs/node

events: initial implementation of experimental EventTarget

Updated to implement more (but not quite all) of Event. There are some variances that need to be reviewed and discussed.

jasnell

comment created time in a day

Pull request review commentnodejs/node

events: initial implementation of experimental EventTarget

 if the `EventEmitter` emits `'error'`. It removes all listeners when exiting the loop. The `value` returned by each iteration is an array composed of the emitted event arguments. +## `EventTarget` and `Event` API+<!-- YAML+added: REPLACEME+-->++> Stability: 1 - Experimental++The global `EventTarget` and `Event` objects are a Node.js-specific adaptation+of the [`EventTarget` Web API][] that are exposed by some Node.js core APIs.+Neither the `EventTarget` nor `Event` classes are currently available for end+user code to create.++```js+const target = getEventTargetSomehow();++target.dispatchEvent({ type: 'foo' });++target.addEventListener('foo', (event) => {+  console.log('foo event happened!');+});+```++The `EventTarget` class is similar in function to `EventEmitter` but evolved+separately as part of browser DOM APIs. The Node.js implementation of+`EventTarget` is an adaptation of the DOM API with a number of Node.js-specific+extensions and behaviors intended to allow it to work more naturally within+the Node.js environment.++### Node.js `EventTarget` vs. DOM `EventTarget`++There are four key differences between the Node.js `EventTarget` and the+[`EventTarget` Web API][]:++1. There is no concept of a hierarchical DOM and event propagation in Node.js.+   That is, an event dispatched to an `EventTarget` does not propagate through+   a hierarchy of nested target objects that may each have their own set of+   handlers for the event.+2. In the Node.js `EventTarget`, any object with a `type` property whose value+   is a string may be dispatched as an `event`.

Noted. We may get there eventually but for now the experimental implementation won't be exposed to user code so we have some wiggle room here.

jasnell

comment created time in a day

pull request commentnodejs/node

events: initial implementation of experimental EventTarget

Why? In the browser, they still return what's passed in, even when the event is used with a null get-the-parent EventTarget...In general, I'm really unclear why not just follow the spec?

To avoid miscommunication. That is, if we don't support Events that bubble, events we produce should never event.bubbles === true. I'm considering going so far as to say that our Event constructor should throw if these known unsupported options are set.

jasnell

comment created time in a day

push eventjasnell/node

James M Snell

commit sha fb8491db0eb7337a409086620e847624be587cc3

[Squash] implement more of Event

view details

push time in a day

pull request commentnodejs/node

events: initial implementation of experimental EventTarget

With regards to non-op properties like bubbles, regardless of whatever value is passed in at the constructor I'd rather the getters just always return false.

jasnell

comment created time in a day

push eventjasnell/node

James M Snell

commit sha c0c9f9db4c8b874c998fe8da3ce87a14c4737716

[Squash] implement a bit more of the Event API

view details

push time in a day

pull request commentnodejs/node

events: initial implementation of experimental EventTarget

@domenic:

It's pretty frustrating to see this get all these non-standard extensions, missing functionality, and semantic mismatches, as compared to the web API.

Keep in mind that this is a work in progress experimental implementation and a key part of this initial discussion is to explore requirements. There is quite a bit of the browser event API that just doesn't make sense in Node.js and implementing everything is not worthwhile for anyone.

It's worthwhile going through the stuff in Event that doesn't seem to make sense here:

Event:

  • Option: bubbles - There is no concept of bubbling/propagating events in Node.js. This option would be a non-op
  • Option: composed - There is no concept of shadow roots in Node.js. This option would be a non-op
  • srcElement, currentTarget, target - We don't currently have a notion of this in Node.js. Once we do, we can easily add these.
  • composedPath() - There is no concept of an invocation path with Node.js events. It wouldn't make sense to implement this.
  • eventPhase - There is no concept of event phases in Node.js and no propagation model. It wouldn't make sense to implement this.
  • stopPropagation()/cancelBubble() - There is no concept of propagation. It wouldn't make sense to implement this.
  • stopImmediatePropagation() - This one we definitely should implement
  • bubbles - No concept of bubbling
  • returnValue - Legacy, doesn't make sense to implement
  • composed - No concept of composition
  • isTrusted - No differentiation between trusted/untrusted events
  • initEvent() - We could implement this if it makes sense to do so
jasnell

comment created time in a day

pull request commentnodejs/node

events: initial implementation of experimental EventTarget

Updated to separate out NodeEventTarget from EventTarget

jasnell

comment created time in a day

Pull request review commentnodejs/node

events: initial implementation of experimental EventTarget

 if the `EventEmitter` emits `'error'`. It removes all listeners when exiting the loop. The `value` returned by each iteration is an array composed of the emitted event arguments. +## `EventTarget` and `Event` API+<!-- YAML+added: REPLACEME+-->++> Stability: 1 - Experimental++The global `EventTarget` and `Event` objects are a Node.js-specific adaptation+of the [`EventTarget` Web API][] that are exposed by some Node.js core APIs.+Neither the `EventTarget` nor `Event` classes are currently available for end+user code to create.++```js+const target = getEventTargetSomehow();++target.dispatchEvent({ type: 'foo' });++target.addEventListener('foo', (event) => {+  console.log('foo event happened!');+});+```++The `EventTarget` class is similar in function to `EventEmitter` but evolved+separately as part of browser DOM APIs. The Node.js implementation of+`EventTarget` is an adaptation of the DOM API with a number of Node.js-specific+extensions and behaviors intended to allow it to work more naturally within+the Node.js environment.++### Node.js `EventTarget` vs. DOM `EventTarget`++There are four key differences between the Node.js `EventTarget` and the+[`EventTarget` Web API][]:++1. There is no concept of a hierarchical DOM and event propagation in Node.js.+   That is, an event dispatched to an `EventTarget` does not propagate through+   a hierarchy of nested target objects that may each have their own set of+   handlers for the event.+2. In the Node.js `EventTarget`, any object with a `type` property whose value+   is a string may be dispatched as an `event`.

The ability to support third-party polyfill type events is exactly the motivation here, fwiw. Especially since we're not exporting the Event constructor right away.

jasnell

comment created time in a day

push eventjasnell/node

James M Snell

commit sha d56fa739b4a91404974ccc88b075d0418355ec8e

[Squash] address feedback Separate `NodeEventTarget` out from `EventTarget`

view details

push time in a day

Pull request review commentnodejs/node

events: initial implementation of experimental EventTarget

+'use strict';++const {+  ArrayFrom,+  Object,+  Map,+  SafeWeakMap,+  Set,+  SymbolFor,+} = primordials;++const {+  codes: {+    ERR_INVALID_ARG_TYPE,+    ERR_INVALID_THIS,+    ERR_EVENT_RECURSION,+  }+} = require('internal/errors');+const kRejection = SymbolFor('nodejs.rejection');++const perf_hooks = require('perf_hooks');+const { customInspectSymbol } = require('internal/util');+const { inspect } = require('util');++const events = new SafeWeakMap();+const targets = new SafeWeakMap();++class Event {+  constructor(type, options) {+    if (typeof type !== 'string')+      throw new ERR_INVALID_ARG_TYPE('type', 'string', type);+    if (options != null && typeof options !== 'object')+      throw new ERR_INVALID_ARG_TYPE('options', 'object', options);+    const { cancelable } = { ...options };+    events.set(this, {+      type,+      defaultPrevented: false,+      cancelable: !!cancelable,+      timestamp: perf_hooks.performance.now()+    });+  }++  [customInspectSymbol]() {+    const obj = {+      type: this.type,+      defaultPrevented: this.defaultPrevented,+      cancelable: this.cancelable,+    };+    return `${this.constructor.name} ${inspect(obj)}`;+  }+}++function preventDefault() {+  const state = events.get(this);+  if (state == null) {+    throw new ERR_INVALID_THIS('Event');+  }+  state.defaultPrevented = true;+}++Object.defineProperties(Event.prototype, {+  type: {+    enumerable: true,+    configurable: true,+    get() {+      const props = events.get(this);+      if (props == null) {+        throw new ERR_INVALID_THIS('Event');+      }+      return props.type;+    }+  },+  cancelable: {+    enumerable: true,+    configurable: true,+    get() {+      const props = events.get(this);+      if (props == null) {+        throw new ERR_INVALID_THIS('Event');+      }+      return props.cancelable;+    }+  },+  defaultPrevented: {+    enumerable: true,+    configurable: true,+    get() {+      const props = events.get(this);+      if (props == null) {+        throw new ERR_INVALID_THIS('Event');+      }+      return props.cancelable && props.defaultPrevented;+    }+  },+  timeStamp: {+    enumerable: true,+    configurable: true,+    get() {+      const props = events.get(this);+      if (props == null) {+        throw new ERR_INVALID_THIS('Event');+      }+      return props.timestamp;+    }+  },+  preventDefault: {+    enumerable: true,+    configurable: true,+    writable: true,+    value: preventDefault+  }+});++class EventTarget {+  constructor() {+    targets.set(this, {+      events: new Map(),+      emitting: new Set()+    });+  }++  static captureRejectionSymbol = kRejection;+}++// EventTarget API++function validateType(type) {+  if (typeof type !== 'string')+    throw new ERR_INVALID_ARG_TYPE('type', 'string', type);+}++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 } = options;+  if (typeof once !== 'boolean')+    throw new ERR_INVALID_ARG_TYPE('options.once', 'boolean', once);+  return { once };+}++function addEventListener(type, listener, options = {}) {+  const state = targets.get(this);+  if (state === undefined)+    throw new ERR_INVALID_THIS('EventTarget');++  validateType(type);+  validateListener(listener);++  const { once } = validateEventListenerOptions(options);++  let handlers = state.events.get(type);+  if (handlers === undefined) {+    handlers = new Map();+    state.events.set(type, handlers);+  }++  if (handlers.has(listener))+    return;++  const callback =+    typeof listener === 'function' ?+      listener.bind(this) :+      listener.handleEvent.bind(listener);++  const handler = {+    callback,+    once,+    remove: removeEventListener.bind(this, type, listener)+  };++  handlers.set(listener, handler);+}++function removeEventListener(type, listener) {+  const state = targets.get(this);+  if (state === undefined)+    throw new ERR_INVALID_THIS('EventTarget');++  validateType(type);+  validateListener(listener);++  const handlers = state.events.get(type);+  if (handlers !== undefined) {+    handlers.delete(listener);+    if (handlers.size === 0)+      state.events.delete(type);+  }+}++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) {+  if (typeof that[kRejection] === 'function') {+    that[kRejection](err, event);+  } else {+    process.emit('uncaughtException', err, event);+  }+}++function dispatchEvent(event) {+  const state = targets.get(this);+  if (state === undefined)+    throw new ERR_INVALID_THIS('EventTarget');++  if (event == null ||+      typeof event !== 'object' ||+      typeof event.type !== 'string') {+    throw new ERR_INVALID_ARG_TYPE('event', 'Event', event);+  }++  if (state.emitting.has(event.type)) {+    throw new ERR_EVENT_RECURSION(event.type);+  }++  const handlers = state.events.get(event.type);+  if (handlers !== undefined) {+    state.emitting.add(event.type);+    try {+      const copy = ArrayFrom(handlers.values());+      for (let n = 0; n < copy.length; n++) {+        if (copy[n].once)+          copy[n].remove();+        try {+          const result = copy[n].callback(event);+          if (result !== undefined && result !== null) {+            addCatch(this, result, event);+          }+        } catch (err) {+          emitUnhandledRejectionOrErr(this, err, event);+        }+      }+    } finally {+      state.emitting.delete(event.type);+    }+  }+}++// EventEmitter-ish API:

Either expose a NodeEventTarget interface that's a subclass of EventTarget that adds these.

I'd prefer this approach.

jasnell

comment created time in a day

push eventjasnell/node

James M Snell

commit sha 0b110675e4125d6f9df24fc8ccaa9305a9b5e31d

[Squash] fixup typo

view details

push time in a day

Pull request review commentnodejs/node

events: initial implementation of experimental EventTarget

 Encoding provided to `TextDecoder()` API was not one of the  `--print` cannot be used with ESM input. +<a id="ERR_EVENT_RECURSION"></a>+### `ERR_EVENT_RECURSION`++Thrown when an attempt is made to recursively dispatch an event on `EventTarget`.

Firefox also prevents this case but doesn't crash.

jasnell

comment created time in a day

Pull request review commentnodejs/node

events: initial implementation of experimental EventTarget

 if the `EventEmitter` emits `'error'`. It removes all listeners when exiting the loop. The `value` returned by each iteration is an array composed of the emitted event arguments. +## `EventTarget` and `Event` API+<!-- YAML+added: REPLACEME+-->++> Stability: 1 - Experimental++The global `EventTarget` and `Event` objects are a Node.js-specific adaptation+of the [`EventTarget` Web API][] that are exposed by some Node.js core APIs.+Neither the `EventTarget` nor `Event` classes are currently available for end+user code to create.++```js+const target = getEventTargetSomehow();++target.dispatchEvent({ type: 'foo' });++target.addEventListener('foo', (event) => {+  console.log('foo event happened!');+});+```++The `EventTarget` class is similar in function to `EventEmitter` but evolved+separately as part of browser DOM APIs. The Node.js implementation of+`EventTarget` is an adaptation of the DOM API with a number of Node.js-specific+extensions and behaviors intended to allow it to work more naturally within+the Node.js environment.++### Node.js `EventTarget` vs. DOM `EventTarget`++There are four key differences between the Node.js `EventTarget` and the+[`EventTarget` Web API][]:++1. There is no concept of a hierarchical DOM and event propagation in Node.js.+   That is, an event dispatched to an `EventTarget` does not propagate through+   a hierarchy of nested target objects that may each have their own set of+   handlers for the event.+2. In the Node.js `EventTarget`, any object with a `type` property whose value+   is a string may be dispatched as an `event`.+3. In the Node.js `EventTarget`, if an event listener is an async function+   or returns a `Promise`, and returned `Promise` rejects, the rejection will+   be automatically captured and handled the same way as a listener that throws+   synchronously (see [`EventTarget` Error Handling][] for details).+4. The Node.js `EventTarget` implements an additional set of APIs that allow it+   to closely emulate a subset of the `EventEmitter` API.

As far as I can see, there is nothing in the spec that prevents EventTarget from being extended. If anything here, we can expose two interfaces: a base EventTarget that exposes the subset of EventTarget APIs that make sense in Node.js and an EmitterEventTarget that extends that to add the additional EventEmitter emulation.

jasnell

comment created time in a day

Pull request review commentnodejs/node

events: initial implementation of experimental EventTarget

 if the `EventEmitter` emits `'error'`. It removes all listeners when exiting the loop. The `value` returned by each iteration is an array composed of the emitted event arguments. +## `EventTarget` and `Event` API+<!-- YAML+added: REPLACEME+-->++> Stability: 1 - Experimental++The global `EventTarget` and `Event` objects are a Node.js-specific adaptation+of the [`EventTarget` Web API][] that are exposed by some Node.js core APIs.+Neither the `EventTarget` nor `Event` classes are currently available for end+user code to create.++```js+const target = getEventTargetSomehow();++target.dispatchEvent({ type: 'foo' });++target.addEventListener('foo', (event) => {+  console.log('foo event happened!');+});+```++The `EventTarget` class is similar in function to `EventEmitter` but evolved+separately as part of browser DOM APIs. The Node.js implementation of+`EventTarget` is an adaptation of the DOM API with a number of Node.js-specific+extensions and behaviors intended to allow it to work more naturally within+the Node.js environment.++### Node.js `EventTarget` vs. DOM `EventTarget`++There are four key differences between the Node.js `EventTarget` and the+[`EventTarget` Web API][]:++1. There is no concept of a hierarchical DOM and event propagation in Node.js.+   That is, an event dispatched to an `EventTarget` does not propagate through+   a hierarchy of nested target objects that may each have their own set of+   handlers for the event.+2. In the Node.js `EventTarget`, any object with a `type` property whose value+   is a string may be dispatched as an `event`.

Understood. This was included specifically because userland implementations support it. Whether we end up supporting it here or not is still to be determined.

jasnell

comment created time in a day

Pull request review commentnodejs/node

events: initial implementation of experimental EventTarget

 if the `EventEmitter` emits `'error'`. It removes all listeners when exiting the loop. The `value` returned by each iteration is an array composed of the emitted event arguments. +## `EventTarget` and `Event` API+<!-- YAML+added: REPLACEME+-->++> Stability: 1 - Experimental++The global `EventTarget` and `Event` objects are a Node.js-specific adaptation+of the [`EventTarget` Web API][] that are exposed by some Node.js core APIs.+Neither the `EventTarget` nor `Event` classes are currently available for end+user code to create.++```js+const target = getEventTargetSomehow();++target.dispatchEvent({ type: 'foo' });++target.addEventListener('foo', (event) => {+  console.log('foo event happened!');+});+```++The `EventTarget` class is similar in function to `EventEmitter` but evolved+separately as part of browser DOM APIs. The Node.js implementation of+`EventTarget` is an adaptation of the DOM API with a number of Node.js-specific+extensions and behaviors intended to allow it to work more naturally within+the Node.js environment.++### Node.js `EventTarget` vs. DOM `EventTarget`++There are four key differences between the Node.js `EventTarget` and the+[`EventTarget` Web API][]:++1. There is no concept of a hierarchical DOM and event propagation in Node.js.+   That is, an event dispatched to an `EventTarget` does not propagate through+   a hierarchy of nested target objects that may each have their own set of+   handlers for the event.

The point remains, the fact that there is no concept of hierarchy at all in Node.js (as opposed to hierarchy being intrinsic in the DOM and only some EventTargets not needing it means that we don't need any of the propagation logic, properties, etc.

jasnell

comment created time in a day

Pull request review commentnodejs/node

events: initial implementation of experimental EventTarget

 Encoding provided to `TextDecoder()` API was not one of the  `--print` cannot be used with ESM input. +<a id="ERR_EVENT_RECURSION"></a>+### `ERR_EVENT_RECURSION`++Thrown when an attempt is made to recursively dispatch an event on `EventTarget`.

Because it causes an infinite recursion. In chrome try each of the following:

const et = new EventTarget();
et.addEventListener('foo', (event) => et.dispatchEvent(event));
et.dispatchEvent(new Event('foo'));
// Reports DOMException, event already being dispatched
const et = new EventTarget();
et.addEventListener('foo', (event) => et.dispatchEvent(new Event('foo')));
et.dispatchEvent(new Event('foo'));
// Causes infinite recursion
jasnell

comment created time in a day

pull request commentnodejs/node

lib: replace charCodeAt with fixed Unicode

Not sure what the concerns are @BridgeAR ... this LGTM

rickyes

comment created time in a day

issue commentnodejs/node

AbortController - Reaffirming promise reject & AbortError semantics

It would be fairly easy for us to expose an AbortError using our current internal errors mechanism. I'd prefer not to extend off DOMException tho.

Bnaya

comment created time in a day

pull request commentnodejs/node

[WIP] src,lib: policy permissions

@bengl yeah I spotted that also. We have a number of such issues currently (e.g. all of our internal uses of process.env). I've been thinking through a number of approaches on this. Internal privileged operations being one possibility. Another would be to implement a kind of run level type of mechanism, where node bootstrap or internal code runs at one run level, and user code runs at another. The permissions would then be applied at the user code run level. That however gets quite a bit more complicated, so I am still evaluating the pros and the cons.

jasnell

comment created time in a day

push eventjasnell/node

James M Snell

commit sha e2578a5fd1307c3738a270b7a1a7cdbdf1ac2239

events: initial implementation of experimental EventTarget See documentation changes for details Signed-off-by: James M Snell <jasnell@gmail.com>

view details

push time in a day

pull request commentnodejs/node

events: initial implementation of experimental EventTarget

The error semantics are definitely something that need to be carefully considered here. In the current implementation, if handler does error, that is not emitted on process.on('error') until process.nextTick(), which means it does not interrupt the execution of the handlers or the current execution scope.

jasnell

comment created time in a day

push eventjasnell/node

James M Snell

commit sha eae033bf6fd9992e52b512fe8114ff27734342b6

[Squash] cleanups

view details

push time in a day

Pull request review commentnodejs/node

events: initial implementation of experimental EventTarget

+'use strict';++const {+  ArrayFrom,+  Object,+  Map,+  SafeWeakMap,+  Set,+  SymbolFor,+} = primordials;++const {+  codes: {+    ERR_INVALID_ARG_TYPE,+    ERR_INVALID_THIS,+    ERR_EVENT_RECURSION,+  }+} = require('internal/errors');+const kRejection = SymbolFor('nodejs.rejection');++const perf_hooks = require('perf_hooks');+const { customInspectSymbol } = require('internal/util');+const { inspect } = require('util');++const events = new SafeWeakMap();+const targets = new SafeWeakMap();++class Event {+  constructor(type, options) {+    if (typeof type !== 'string')+      throw new ERR_INVALID_ARG_TYPE('type', 'string', type);+    if (options != null && typeof options !== 'object')+      throw new ERR_INVALID_ARG_TYPE('options', 'object', options);+    const { cancelable } = { ...options };+    events.set(this, {+      type,+      defaultPrevented: false,+      cancelable: !!cancelable,+      timestamp: perf_hooks.performance.now()+    });+  }++  [customInspectSymbol]() {+    const obj = {+      type: this.type,+      defaultPrevented: this.defaultPrevented,+      cancelable: this.cancelable,+    };+    return `${this.constructor.name} ${inspect(obj)}`;+  }+}++function preventDefault() {+  const state = events.get(this);+  if (state == null) {+    throw new ERR_INVALID_THIS('Event');+  }+  state.defaultPrevented = true;+}++Object.defineProperties(Event.prototype, {+  type: {+    enumerable: true,+    configurable: true,+    get() {+      const props = events.get(this);+      if (props == null) {+        throw new ERR_INVALID_THIS('Event');+      }+      return props.type;+    }+  },+  cancelable: {+    enumerable: true,+    configurable: true,+    get() {+      const props = events.get(this);+      if (props == null) {+        throw new ERR_INVALID_THIS('Event');+      }+      return props.cancelable;+    }+  },+  defaultPrevented: {+    enumerable: true,+    configurable: true,+    get() {+      const props = events.get(this);+      if (props == null) {+        throw new ERR_INVALID_THIS('Event');+      }+      return props.cancelable && props.defaultPrevented;+    }+  },+  timeStamp: {+    enumerable: true,+    configurable: true,+    get() {+      const props = events.get(this);+      if (props == null) {+        throw new ERR_INVALID_THIS('Event');+      }+      return props.timestamp;+    }+  },+  preventDefault: {+    enumerable: true,+    configurable: true,+    writable: true,+    value: preventDefault+  }+});++class EventTarget {+  constructor() {+    targets.set(this, {+      events: new Map(),+      emitting: new Set()+    });+  }++  static captureRejectionSymbol = kRejection;+}++// EventTarget API++function validateType(type) {+  if (typeof type !== 'string')+    throw new ERR_INVALID_ARG_TYPE('type', 'string', type);+}++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 } = options;+  if (typeof once !== 'boolean')+    throw new ERR_INVALID_ARG_TYPE('options.once', 'boolean', once);+  return { once };+}++function addEventListener(type, listener, options = {}) {+  const state = targets.get(this);+  if (state === undefined)+    throw new ERR_INVALID_THIS('EventTarget');++  validateType(type);+  validateListener(listener);++  const { once } = validateEventListenerOptions(options);++  let handlers = state.events.get(type);+  if (handlers === undefined) {+    handlers = new Map();+    state.events.set(type, handlers);+  }++  if (handlers.has(listener))+    return;++  const callback =+    typeof listener === 'function' ?+      listener.bind(this) :+      listener.handleEvent.bind(listener);++  const handler = {+    callback,+    once,+    remove: removeEventListener.bind(this, type, listener)+  };++  handlers.set(listener, handler);+}++function removeEventListener(type, listener) {+  const state = targets.get(this);+  if (state === undefined)+    throw new ERR_INVALID_THIS('EventTarget');++  validateType(type);+  validateListener(listener);++  const handlers = state.events.get(type);+  if (handlers !== undefined) {+    handlers.delete(listener);+    if (handlers.size === 0)+      state.events.delete(type);+  }+}++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) {+  if (typeof that[kRejection] === 'function') {+    that[kRejection](err, event);+  } else {+    process.emit('uncaughtException', err, event);+  }+}++function dispatchEvent(event) {+  const state = targets.get(this);+  if (state === undefined)+    throw new ERR_INVALID_THIS('EventTarget');++  if (event == null ||+      typeof event !== 'object' ||+      typeof event.type !== 'string') {+    throw new ERR_INVALID_ARG_TYPE('event', 'Event', event);+  }++  if (state.emitting.has(event.type)) {+    throw new ERR_EVENT_RECURSION(event.type);+  }++  const handlers = state.events.get(event.type);+  if (handlers !== undefined) {+    state.emitting.add(event.type);+    try {+      const copy = ArrayFrom(handlers.values());+      for (let n = 0; n < copy.length; n++) {+        if (copy[n].once)+          copy[n].remove();+        try {+          const result = copy[n].callback(event);+          if (result !== undefined && result !== null) {+            addCatch(this, result, event);+          }+        } catch (err) {+          emitUnhandledRejectionOrErr(this, err, event);+        }+      }+    } finally {+      state.emitting.delete(event.type);+    }+  }+}++// EventEmitter-ish API:

Given that these already exist in EventEmitter and the idea here is to emulate EventEmitter, this is not making any of that any worse (or better) than it already is.

jasnell

comment created time in a day

push eventjasnell/node

James M Snell

commit sha aca8919e22221b970369bb3817408c377e1d917a

[Squash] Add EventEmitter observability emulation

view details

push time in a day

issue commentbikeshaving/crank

EventTarget + Event + CustomEvent in all environments

Note that in the PRs current state, the Event constructor will not be exposed to userland. However, userland code will be able to call dispatchEvent with any object that has a type property whose value is a string

ryhinchey

comment created time in a day

Pull request review commentnodejs/node

events: initial implementation of experimental EventTarget

+'use strict';++const {+  ArrayFrom,+  Object,+  Map,+  SafeWeakMap,+  Set,+  SymbolFor,+} = primordials;++const {+  codes: {+    ERR_INVALID_ARG_TYPE,+    ERR_INVALID_THIS,+    ERR_EVENT_RECURSION,+  }+} = require('internal/errors');+const kRejection = SymbolFor('nodejs.rejection');++const perf_hooks = require('perf_hooks');+const { customInspectSymbol } = require('internal/util');+const { inspect } = require('util');++const events = new SafeWeakMap();+const targets = new SafeWeakMap();++class Event {+  constructor(type, options) {+    if (typeof type !== 'string')+      throw new ERR_INVALID_ARG_TYPE('type', 'string', type);+    if (options != null && typeof options !== 'object')+      throw new ERR_INVALID_ARG_TYPE('options', 'object', options);+    const { cancelable } = { ...options };+    events.set(this, {+      type,+      defaultPrevented: false,+      cancelable: !!cancelable,+      timestamp: perf_hooks.performance.now()+    });+  }++  [customInspectSymbol]() {+    const obj = {+      type: this.type,+      defaultPrevented: this.defaultPrevented,+      cancelable: this.cancelable,+    };+    return `${this.constructor.name} ${inspect(obj)}`;+  }+}++function preventDefault() {+  const state = events.get(this);+  if (state == null) {+    throw new ERR_INVALID_THIS('Event');+  }+  state.defaultPrevented = true;+}++Object.defineProperties(Event.prototype, {+  type: {+    enumerable: true,+    configurable: true,+    get() {+      const props = events.get(this);+      if (props == null) {+        throw new ERR_INVALID_THIS('Event');+      }+      return props.type;+    }+  },+  cancelable: {+    enumerable: true,+    configurable: true,+    get() {+      const props = events.get(this);+      if (props == null) {+        throw new ERR_INVALID_THIS('Event');+      }+      return props.cancelable;+    }+  },+  defaultPrevented: {+    enumerable: true,+    configurable: true,+    get() {+      const props = events.get(this);+      if (props == null) {+        throw new ERR_INVALID_THIS('Event');+      }+      return props.cancelable && props.defaultPrevented;+    }+  },+  timeStamp: {+    enumerable: true,+    configurable: true,+    get() {+      const props = events.get(this);+      if (props == null) {+        throw new ERR_INVALID_THIS('Event');+      }+      return props.timestamp;+    }+  },+  preventDefault: {+    enumerable: true,+    configurable: true,+    writable: true,+    value: preventDefault+  }+});++class EventTarget {+  constructor() {+    targets.set(this, {+      events: new Map(),+      emitting: new Set()+    });+  }++  static captureRejectionSymbol = kRejection;+}++// EventTarget API++function validateType(type) {+  if (typeof type !== 'string')+    throw new ERR_INVALID_ARG_TYPE('type', 'string', type);+}++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 } = options;+  if (typeof once !== 'boolean')+    throw new ERR_INVALID_ARG_TYPE('options.once', 'boolean', once);+  return { once };+}++function addEventListener(type, listener, options = {}) {+  const state = targets.get(this);+  if (state === undefined)+    throw new ERR_INVALID_THIS('EventTarget');++  validateType(type);+  validateListener(listener);++  const { once } = validateEventListenerOptions(options);++  let handlers = state.events.get(type);+  if (handlers === undefined) {+    handlers = new Map();+    state.events.set(type, handlers);+  }++  if (handlers.has(listener))+    return;++  const callback =+    typeof listener === 'function' ?+      listener.bind(this) :+      listener.handleEvent.bind(listener);++  const handler = {+    callback,+    once,+    remove: removeEventListener.bind(this, type, listener)+  };++  handlers.set(listener, handler);+}++function removeEventListener(type, listener) {+  const state = targets.get(this);+  if (state === undefined)+    throw new ERR_INVALID_THIS('EventTarget');++  validateType(type);+  validateListener(listener);++  const handlers = state.events.get(type);+  if (handlers !== undefined) {+    handlers.delete(listener);+    if (handlers.size === 0)+      state.events.delete(type);+  }+}++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) {+  if (typeof that[kRejection] === 'function') {+    that[kRejection](err, event);+  } else {+    process.emit('uncaughtException', err, event);+  }+}++function dispatchEvent(event) {+  const state = targets.get(this);+  if (state === undefined)+    throw new ERR_INVALID_THIS('EventTarget');++  if (event == null ||+      typeof event !== 'object' ||+      typeof event.type !== 'string') {+    throw new ERR_INVALID_ARG_TYPE('event', 'Event', event);+  }++  if (state.emitting.has(event.type)) {+    throw new ERR_EVENT_RECURSION(event.type);+  }++  const handlers = state.events.get(event.type);+  if (handlers !== undefined) {+    state.emitting.add(event.type);+    try {+      const copy = ArrayFrom(handlers.values());+      for (let n = 0; n < copy.length; n++) {+        if (copy[n].once)+          copy[n].remove();+        try {+          const result = copy[n].callback(event);+          if (result !== undefined && result !== null) {+            addCatch(this, result, event);+          }+        } catch (err) {+          emitUnhandledRejectionOrErr(this, err, event);+        }+      }+    } finally {+      state.emitting.delete(event.type);+    }+  }+}++// EventEmitter-ish API:

@ljharb ... for DOM EventTarget and everything that entails, I would agree. the Node.js implementation, not exposing these additional functions makes the EventTarget far more likely to become a memory leak. I'd prefer to keep the hybrid EventTarget/EventEmitter behavior here with explicit recognition in the docs that the behavior (and requirements) are different here.

jasnell

comment created time in a day

Pull request review commentnodejs/node

events: initial implementation of experimental EventTarget

 if the `EventEmitter` emits `'error'`. It removes all listeners when exiting the loop. The `value` returned by each iteration is an array composed of the emitted event arguments. +## `EventTarget` and `Event` API+<!-- YAML+added: REPLACEME+-->++> Stability: 1 - Experimental++The global `EventTarget` and `Event` objects are a Node.js-specific adaptation+of the [`EventTarget` Web API][].++```js+const eventTarget = new EventTarget();+const event = new Event('foo');++eventTarget.addEventListener('foo', (event) => {+  console.log(`The ${event.type} event happened`);+});+```++The `EventTarget` class is similar in function to `EventEmitter` by evolved+separately as part of browser DOM APIs. The Node.js implementation of+`EventTarget` is an adaptation of the DOM API with a number of Node.js-specific+extensions and behaviors intended to allow it to work more naturally within+the Node.js environment.++### Node.js `EventTarget` vs. DOM `EventTarget`++There are four key differences between the Node.js `EventTarget` and the+[`EventTarget` Web API][]:++1. There is no concept of a hierarchical DOM and event propagation in Node.js.+   That is, an event dispatched to an `EventTarget` does not propagate through+   a hierarchy of nested target objects that may each have their own set of+   handlers for the event.+2. In the Node.js `EventTarget`, any object with a `type` property whose value+   is a string may be dispatched as an `event`.+3. In the Node.js `EventTarget`, if an event listener is an async function+   or returns a `Promise`, and returned `Promise` rejects, the rejection will+   be automatically captured and handled the same way as a listener that throws+   synchronously (see [`EventTarget` Error Handling][] for details).+4. The Node.js `EventTarget` implements an additional set of APIs that allow it+   to closely emulate a subset of the `EventEmitter` API.++### `EventTarget` vs. `EventEmitter`++The Node.js `EventTarget` object implements a modified subset of the+`EventEmitter` API that allows it to closely emulate an `EventEmitter` in+certain situations. It is important to understand, however, that an+`EventTarget` is *not* an instance of `EventEmitter` and cannot be used in+place of an `EventEmitter` in most cases.++1. Unlike `EventEmitter`, any given `listener` can be registered at most once+   per event `type`. Attempts to register a `listener` multiple times will be+   ignored.+2. While the Node.js `EventTarget` exposes an `emit()` function that uses the+   same signature as `EventEmitter`, the event will be dispatched to registered+   listeners as a single event object composed as an array with an additional+   `type` property rather than as separate arguments.+3. The Node.js `EventTarget` does not emulate the full `EventEmitter` API.+   Specifically the `prependListener()`, `prependOnceListener()`,+   `rawListeners()`, `setMaxListeners()`, `getMaxListeners()`, and+   `errorMonitor` APIs are not emulated.+4. The Node.js `EventTarget` does not implement any special default behavior+   for events with type `'error'`.+5. The Node.js `EventTarget` supports `EventListener` objects as well as+   functions as handlers for all event types.++### Event Listener++Event listeners registered for an event `type` may either be JavaScript+functions or objects with a `handleEvent` property whose value is a function.++In either case, the handler function will be invoked with the `event` argument+passed to the `eventTarget.dispatchEvent()` function.++Async functions may be used as event listeners. If an async handler function+rejects, the rejection will be captured and be will handled as described in+[`EventTarget` Error Handling][].++An error thrown by one handler function will not prevent the other handlers+from being invoked.++The return value of a handler function will be ignored.++Handlers are always invoked in the order they were added.++Handler functions may mutate the `event` object.++```js+function handler1(event) {+  console.log(event.type);  // Prints 'foo'+  event.a = 1;+}++async function handler2(event) {+  console.log(event.type);  // Prints 'foo'+  console.log(event.a);  // Prints 1+}++const handler3 = {+  handleEvent(event) {+    console.log(event.type);  // Prints 'foo'+  }+};++const handler4 = {+  async handleEvent(event) {+    console.log(event.type);  // Prints 'foo'+  }+};++const target = new EventTarget();+target.addEventListener('foo', handler1);+target.addEventListener('foo', handler2);+target.addEventListener('foo', handler3);+target.addEventListener('foo', handler4, { once: true });++target.dispatchEvent({ type: 'foo' });+```++When the `eventTarget.emit()` function is used. the arguments passed

Given that we're not exposing the EventTarget constructor, this makes less sense to expose the emit()... removed!

jasnell

comment created time in a day

Pull request review commentnodejs/node

events: initial implementation of experimental EventTarget

 if the `EventEmitter` emits `'error'`. It removes all listeners when exiting the loop. The `value` returned by each iteration is an array composed of the emitted event arguments. +## `EventTarget` and `Event` API+<!-- YAML+added: REPLACEME+-->++> Stability: 1 - Experimental++The global `EventTarget` and `Event` objects are a Node.js-specific adaptation+of the [`EventTarget` Web API][].++```js+const eventTarget = new EventTarget();+const event = new Event('foo');++eventTarget.addEventListener('foo', (event) => {+  console.log(`The ${event.type} event happened`);+});+```++The `EventTarget` class is similar in function to `EventEmitter` by evolved+separately as part of browser DOM APIs. The Node.js implementation of+`EventTarget` is an adaptation of the DOM API with a number of Node.js-specific+extensions and behaviors intended to allow it to work more naturally within+the Node.js environment.++### Node.js `EventTarget` vs. DOM `EventTarget`++There are four key differences between the Node.js `EventTarget` and the+[`EventTarget` Web API][]:++1. There is no concept of a hierarchical DOM and event propagation in Node.js.+   That is, an event dispatched to an `EventTarget` does not propagate through+   a hierarchy of nested target objects that may each have their own set of+   handlers for the event.+2. In the Node.js `EventTarget`, any object with a `type` property whose value+   is a string may be dispatched as an `event`.+3. In the Node.js `EventTarget`, if an event listener is an async function+   or returns a `Promise`, and returned `Promise` rejects, the rejection will+   be automatically captured and handled the same way as a listener that throws+   synchronously (see [`EventTarget` Error Handling][] for details).+4. The Node.js `EventTarget` implements an additional set of APIs that allow it+   to closely emulate a subset of the `EventEmitter` API.++### `EventTarget` vs. `EventEmitter`++The Node.js `EventTarget` object implements a modified subset of the+`EventEmitter` API that allows it to closely emulate an `EventEmitter` in+certain situations. It is important to understand, however, that an+`EventTarget` is *not* an instance of `EventEmitter` and cannot be used in+place of an `EventEmitter` in most cases.++1. Unlike `EventEmitter`, any given `listener` can be registered at most once+   per event `type`. Attempts to register a `listener` multiple times will be+   ignored.+2. While the Node.js `EventTarget` exposes an `emit()` function that uses the+   same signature as `EventEmitter`, the event will be dispatched to registered+   listeners as a single event object composed as an array with an additional+   `type` property rather than as separate arguments.+3. The Node.js `EventTarget` does not emulate the full `EventEmitter` API.+   Specifically the `prependListener()`, `prependOnceListener()`,+   `rawListeners()`, `setMaxListeners()`, `getMaxListeners()`, and+   `errorMonitor` APIs are not emulated.+4. The Node.js `EventTarget` does not implement any special default behavior+   for events with type `'error'`.+5. The Node.js `EventTarget` supports `EventListener` objects as well as+   functions as handlers for all event types.++### Event Listener++Event listeners registered for an event `type` may either be JavaScript+functions or objects with a `handleEvent` property whose value is a function.++In either case, the handler function will be invoked with the `event` argument+passed to the `eventTarget.dispatchEvent()` function.++Async functions may be used as event listeners. If an async handler function+rejects, the rejection will be captured and be will handled as described in+[`EventTarget` Error Handling][].++An error thrown by one handler function will not prevent the other handlers

Updated to emit process.on('error') instead.

jasnell

comment created time in a day

Pull request review commentnodejs/node

events: initial implementation of experimental EventTarget

 if the `EventEmitter` emits `'error'`. It removes all listeners when exiting the loop. The `value` returned by each iteration is an array composed of the emitted event arguments. +## `EventTarget` and `Event` API+<!-- YAML+added: REPLACEME+-->++> Stability: 1 - Experimental++The global `EventTarget` and `Event` objects are a Node.js-specific adaptation

Ok, updated to not expose the Event and EventTarget constructors.

jasnell

comment created time in a day

Pull request review commentnodejs/node

events: initial implementation of experimental EventTarget

+'use strict';++const {+  ArrayFrom,+  Object,+  Map,+  SafeWeakMap,+  Set,+  SymbolFor,+} = primordials;++const {+  codes: {+    ERR_INVALID_ARG_TYPE,+    ERR_INVALID_THIS,+    ERR_EVENT_RECURSION,+  }+} = require('internal/errors');+const kRejection = SymbolFor('nodejs.rejection');++const perf_hooks = require('perf_hooks');+const { customInspectSymbol } = require('internal/util');+const { inspect } = require('util');++const events = new SafeWeakMap();+const targets = new SafeWeakMap();++class Event {+  constructor(type, options) {+    if (typeof type !== 'string')+      throw new ERR_INVALID_ARG_TYPE('type', 'string', type);+    if (options != null && typeof options !== 'object')+      throw new ERR_INVALID_ARG_TYPE('options', 'object', options);+    const { cancelable } = { ...options };+    events.set(this, {+      type,+      defaultPrevented: false,+      cancelable: !!cancelable,+      timestamp: perf_hooks.performance.now()+    });+  }++  [customInspectSymbol]() {+    const obj = {+      type: this.type,+      defaultPrevented: this.defaultPrevented,+      cancelable: this.cancelable,+    };+    return `${this.constructor.name} ${inspect(obj)}`;+  }+}++function preventDefault() {+  const state = events.get(this);+  if (state == null) {+    throw new ERR_INVALID_THIS('Event');+  }+  state.defaultPrevented = true;+}++Object.defineProperties(Event.prototype, {+  type: {+    enumerable: true,+    configurable: true,+    get() {+      const props = events.get(this);+      if (props == null) {+        throw new ERR_INVALID_THIS('Event');+      }+      return props.type;+    }+  },+  cancelable: {+    enumerable: true,+    configurable: true,+    get() {+      const props = events.get(this);+      if (props == null) {+        throw new ERR_INVALID_THIS('Event');+      }+      return props.cancelable;+    }+  },+  defaultPrevented: {+    enumerable: true,+    configurable: true,+    get() {+      const props = events.get(this);+      if (props == null) {+        throw new ERR_INVALID_THIS('Event');+      }+      return props.cancelable && props.defaultPrevented;+    }+  },+  timeStamp: {+    enumerable: true,+    configurable: true,+    get() {+      const props = events.get(this);+      if (props == null) {+        throw new ERR_INVALID_THIS('Event');+      }+      return props.timestamp;+    }+  },+  preventDefault: {+    enumerable: true,+    configurable: true,+    writable: true,+    value: preventDefault+  }+});++class EventTarget {+  constructor() {+    targets.set(this, {+      events: new Map(),+      emitting: new Set()+    });+  }++  static captureRejectionSymbol = kRejection;+}++// EventTarget API++function validateType(type) {+  if (typeof type !== 'string')+    throw new ERR_INVALID_ARG_TYPE('type', 'string', type);+}++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 } = options;+  if (typeof once !== 'boolean')+    throw new ERR_INVALID_ARG_TYPE('options.once', 'boolean', once);+  return { once };+}++function addEventListener(type, listener, options = {}) {+  const state = targets.get(this);+  if (state === undefined)+    throw new ERR_INVALID_THIS('EventTarget');++  validateType(type);+  validateListener(listener);++  const { once } = validateEventListenerOptions(options);++  let handlers = state.events.get(type);+  if (handlers === undefined) {+    handlers = new Map();+    state.events.set(type, handlers);+  }++  if (handlers.has(listener))+    return;++  const callback =+    typeof listener === 'function' ?+      listener.bind(this) :+      listener.handleEvent.bind(listener);++  const handler = {+    callback,+    once,+    remove: removeEventListener.bind(this, type, listener)+  };++  handlers.set(listener, handler);+}++function removeEventListener(type, listener) {+  const state = targets.get(this);+  if (state === undefined)+    throw new ERR_INVALID_THIS('EventTarget');++  validateType(type);+  validateListener(listener);++  const handlers = state.events.get(type);+  if (handlers !== undefined) {+    handlers.delete(listener);+    if (handlers.size === 0)+      state.events.delete(type);+  }+}++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) {+  if (typeof that[kRejection] === 'function') {+    that[kRejection](err, event);+  } else {+    process.emit('uncaughtException', err, event);+  }+}++function dispatchEvent(event) {+  const state = targets.get(this);+  if (state === undefined)+    throw new ERR_INVALID_THIS('EventTarget');++  if (event == null ||+      typeof event !== 'object' ||+      typeof event.type !== 'string') {+    throw new ERR_INVALID_ARG_TYPE('event', 'Event', event);+  }++  if (state.emitting.has(event.type)) {+    throw new ERR_EVENT_RECURSION(event.type);+  }++  const handlers = state.events.get(event.type);+  if (handlers !== undefined) {+    state.emitting.add(event.type);+    try {+      const copy = ArrayFrom(handlers.values());+      for (let n = 0; n < copy.length; n++) {+        if (copy[n].once)+          copy[n].remove();+        try {+          const result = copy[n].callback(event);+          if (result !== undefined && result !== null) {+            addCatch(this, result, event);+          }+        } catch (err) {+          emitUnhandledRejectionOrErr(this, err, event);+        }+      }+    } finally {+      state.emitting.delete(event.type);+    }+  }+}++// EventEmitter-ish API:++function eventNames() {+  const state = targets.get(this);+  if (state === undefined)+    throw new ERR_INVALID_THIS('EventTarget');+  return ArrayFrom(state.events.keys());+}++function listenerCount(type) {+  const state = targets.get(this);+  if (state === undefined)+    throw new ERR_INVALID_THIS('EventTarget');+  validateType(type);+  const handlers = state.events.get(type);+  return handlers !== undefined ? handlers.size : 0;+}++function off(type, listener) {+  this.removeEventListener(type, listener);+  return this;+}++function on(type, listener) {+  this.addEventListener(type, listener);+  return this;+}

The behave like EventTarget implementation is far simpler so unless we get someone specifically requesting to be able to add a single handler multiple times (and has a good reason for doing so), let's keep it the way it is.

jasnell

comment created time in a day

push eventjasnell/node

James M Snell

commit sha 3d117da40b67f768ba14c8771c223b488010b6fe

[Squash] address feedback * Do not expose `Event` and `EventTarget` constructors (these would be created internally by Node.js APIs but not available for user code to create) * Do not expose emit() function. Since these are used internally, this makes less sense. * Do not implement the rejection override behavior. Since users are not expected to create instances themselves, this makes less sense. * Forward errors to process.on('error')

view details

push time in a day

Pull request review commentnodejs/node

lib: initial experimental AbortController implementation

+'use strict';++// Modeled very closely on the AbortController implementation+// in https://github.com/mysticatea/abort-controller (MIT license)++const {+  SafeWeakMap,+  Object,+  Symbol,+} = primordials;+const EventEmitter = require('events');+const {+  codes: { ERR_INVALID_THIS }+} = require('internal/errors');++const signals = new SafeWeakMap();

Yep, that's the intent but I want to benchmark a few things first.

jasnell

comment created time in a day

Pull request review commentnodejs/node

lib: initial experimental AbortController implementation

+'use strict';++require('../common');++const { ok, CallTracker, throws } = require('assert');++const tracker = new CallTracker();+process.on('exit', () => tracker.verify());++{+  const ac = new AbortController();+  ok(ac.signal);+  ac.signal.onabort = tracker.calls();+  ac.signal.once('abort', tracker.calls());

By default it watches for exactly one call

jasnell

comment created time in a day

Pull request review commentnodejs/node

events: initial implementation of experimental EventTarget

+'use strict';

Let's old off, but it would be good to know which WPT tests for EventTarget and Event we don't pass.

jasnell

comment created time in 2 days

Pull request review commentnodejs/node

events: initial implementation of experimental EventTarget

+'use strict';++const {+  ArrayFrom,+  Object,+  Map,+  SafeWeakMap,+  Set,+  SymbolFor,+} = primordials;++const {+  codes: {+    ERR_INVALID_ARG_TYPE,+    ERR_INVALID_THIS,+    ERR_EVENT_RECURSION,+  }+} = require('internal/errors');+const kRejection = SymbolFor('nodejs.rejection');++const perf_hooks = require('perf_hooks');+const { customInspectSymbol } = require('internal/util');+const { inspect } = require('util');++const events = new SafeWeakMap();+const targets = new SafeWeakMap();++class Event {+  constructor(type, options) {+    if (typeof type !== 'string')+      throw new ERR_INVALID_ARG_TYPE('type', 'string', type);+    if (options != null && typeof options !== 'object')+      throw new ERR_INVALID_ARG_TYPE('options', 'object', options);+    const { cancelable } = { ...options };+    events.set(this, {+      type,+      defaultPrevented: false,+      cancelable: !!cancelable,+      timestamp: perf_hooks.performance.now()+    });+  }++  [customInspectSymbol]() {+    const obj = {+      type: this.type,+      defaultPrevented: this.defaultPrevented,+      cancelable: this.cancelable,+    };+    return `${this.constructor.name} ${inspect(obj)}`;+  }+}++function preventDefault() {+  const state = events.get(this);+  if (state == null) {+    throw new ERR_INVALID_THIS('Event');+  }+  state.defaultPrevented = true;+}++Object.defineProperties(Event.prototype, {+  type: {+    enumerable: true,+    configurable: true,+    get() {+      const props = events.get(this);+      if (props == null) {+        throw new ERR_INVALID_THIS('Event');+      }+      return props.type;+    }+  },+  cancelable: {+    enumerable: true,+    configurable: true,+    get() {+      const props = events.get(this);+      if (props == null) {+        throw new ERR_INVALID_THIS('Event');+      }+      return props.cancelable;+    }+  },+  defaultPrevented: {+    enumerable: true,+    configurable: true,+    get() {+      const props = events.get(this);+      if (props == null) {+        throw new ERR_INVALID_THIS('Event');+      }+      return props.cancelable && props.defaultPrevented;+    }+  },+  timeStamp: {+    enumerable: true,+    configurable: true,+    get() {+      const props = events.get(this);+      if (props == null) {+        throw new ERR_INVALID_THIS('Event');+      }+      return props.timestamp;+    }+  },+  preventDefault: {+    enumerable: true,+    configurable: true,+    writable: true,+    value: preventDefault+  }+});++class EventTarget {+  constructor() {+    targets.set(this, {+      events: new Map(),+      emitting: new Set()+    });+  }++  static captureRejectionSymbol = kRejection;+}++// EventTarget API++function validateType(type) {+  if (typeof type !== 'string')+    throw new ERR_INVALID_ARG_TYPE('type', 'string', type);+}++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 } = options;+  if (typeof once !== 'boolean')+    throw new ERR_INVALID_ARG_TYPE('options.once', 'boolean', once);+  return { once };+}++function addEventListener(type, listener, options = {}) {+  const state = targets.get(this);+  if (state === undefined)+    throw new ERR_INVALID_THIS('EventTarget');++  validateType(type);+  validateListener(listener);++  const { once } = validateEventListenerOptions(options);++  let handlers = state.events.get(type);+  if (handlers === undefined) {+    handlers = new Map();+    state.events.set(type, handlers);+  }++  if (handlers.has(listener))+    return;++  const callback =+    typeof listener === 'function' ?+      listener.bind(this) :+      listener.handleEvent.bind(listener);++  const handler = {+    callback,+    once,+    remove: removeEventListener.bind(this, type, listener)+  };++  handlers.set(listener, handler);+}++function removeEventListener(type, listener) {+  const state = targets.get(this);+  if (state === undefined)+    throw new ERR_INVALID_THIS('EventTarget');++  validateType(type);+  validateListener(listener);++  const handlers = state.events.get(type);+  if (handlers !== undefined) {+    handlers.delete(listener);+    if (handlers.size === 0)+      state.events.delete(type);+  }+}++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) {+  if (typeof that[kRejection] === 'function') {+    that[kRejection](err, event);+  } else {+    process.emit('uncaughtException', err, event);+  }+}++function dispatchEvent(event) {+  const state = targets.get(this);+  if (state === undefined)+    throw new ERR_INVALID_THIS('EventTarget');++  if (event == null ||+      typeof event !== 'object' ||+      typeof event.type !== 'string') {+    throw new ERR_INVALID_ARG_TYPE('event', 'Event', event);+  }++  if (state.emitting.has(event.type)) {+    throw new ERR_EVENT_RECURSION(event.type);+  }++  const handlers = state.events.get(event.type);+  if (handlers !== undefined) {+    state.emitting.add(event.type);+    try {+      const copy = ArrayFrom(handlers.values());+      for (let n = 0; n < copy.length; n++) {+        if (copy[n].once)+          copy[n].remove();+        try {+          const result = copy[n].callback(event);+          if (result !== undefined && result !== null) {+            addCatch(this, result, event);+          }+        } catch (err) {+          emitUnhandledRejectionOrErr(this, err, event);+        }+      }+    } finally {+      state.emitting.delete(event.type);+    }+  }+}++// EventEmitter-ish API:

In this case, given that we're being very explicit that this EventTarget diverges from the DOM EventTarget, and given the considerations around being at least somewhat compatible with EventEmitter, I'd rather we exposed these. Doing so is compatible with userland implementations (the signal in the abort-controller userland module, for instance, implements both EventEmitter and EventTarget APIs).

jasnell

comment created time in 2 days

Pull request review commentnodejs/node

events: initial implementation of experimental EventTarget

 if the `EventEmitter` emits `'error'`. It removes all listeners when exiting the loop. The `value` returned by each iteration is an array composed of the emitted event arguments. +## `EventTarget` and `Event` API+<!-- YAML+added: REPLACEME+-->++> Stability: 1 - Experimental++The global `EventTarget` and `Event` objects are a Node.js-specific adaptation+of the [`EventTarget` Web API][].++```js+const eventTarget = new EventTarget();+const event = new Event('foo');++eventTarget.addEventListener('foo', (event) => {+  console.log(`The ${event.type} event happened`);+});+```++The `EventTarget` class is similar in function to `EventEmitter` by evolved+separately as part of browser DOM APIs. The Node.js implementation of+`EventTarget` is an adaptation of the DOM API with a number of Node.js-specific+extensions and behaviors intended to allow it to work more naturally within+the Node.js environment.++### Node.js `EventTarget` vs. DOM `EventTarget`++There are four key differences between the Node.js `EventTarget` and the+[`EventTarget` Web API][]:++1. There is no concept of a hierarchical DOM and event propagation in Node.js.+   That is, an event dispatched to an `EventTarget` does not propagate through+   a hierarchy of nested target objects that may each have their own set of+   handlers for the event.+2. In the Node.js `EventTarget`, any object with a `type` property whose value+   is a string may be dispatched as an `event`.+3. In the Node.js `EventTarget`, if an event listener is an async function+   or returns a `Promise`, and returned `Promise` rejects, the rejection will+   be automatically captured and handled the same way as a listener that throws+   synchronously (see [`EventTarget` Error Handling][] for details).+4. The Node.js `EventTarget` implements an additional set of APIs that allow it+   to closely emulate a subset of the `EventEmitter` API.++### `EventTarget` vs. `EventEmitter`++The Node.js `EventTarget` object implements a modified subset of the+`EventEmitter` API that allows it to closely emulate an `EventEmitter` in+certain situations. It is important to understand, however, that an+`EventTarget` is *not* an instance of `EventEmitter` and cannot be used in+place of an `EventEmitter` in most cases.++1. Unlike `EventEmitter`, any given `listener` can be registered at most once+   per event `type`. Attempts to register a `listener` multiple times will be+   ignored.+2. While the Node.js `EventTarget` exposes an `emit()` function that uses the+   same signature as `EventEmitter`, the event will be dispatched to registered+   listeners as a single event object composed as an array with an additional+   `type` property rather than as separate arguments.+3. The Node.js `EventTarget` does not emulate the full `EventEmitter` API.+   Specifically the `prependListener()`, `prependOnceListener()`,+   `rawListeners()`, `setMaxListeners()`, `getMaxListeners()`, and+   `errorMonitor` APIs are not emulated.+4. The Node.js `EventTarget` does not implement any special default behavior+   for events with type `'error'`.+5. The Node.js `EventTarget` supports `EventListener` objects as well as+   functions as handlers for all event types.++### Event Listener++Event listeners registered for an event `type` may either be JavaScript+functions or objects with a `handleEvent` property whose value is a function.++In either case, the handler function will be invoked with the `event` argument+passed to the `eventTarget.dispatchEvent()` function.++Async functions may be used as event listeners. If an async handler function+rejects, the rejection will be captured and be will handled as described in+[`EventTarget` Error Handling][].++An error thrown by one handler function will not prevent the other handlers

We could easily emit process.on('error') here instead.

jasnell

comment created time in 2 days

Pull request review commentnodejs/node

events: initial implementation of experimental EventTarget

 if the `EventEmitter` emits `'error'`. It removes all listeners when exiting the loop. The `value` returned by each iteration is an array composed of the emitted event arguments. +## `EventTarget` and `Event` API+<!-- YAML+added: REPLACEME+-->++> Stability: 1 - Experimental++The global `EventTarget` and `Event` objects are a Node.js-specific adaptation+of the [`EventTarget` Web API][].++```js+const eventTarget = new EventTarget();+const event = new Event('foo');++eventTarget.addEventListener('foo', (event) => {+  console.log(`The ${event.type} event happened`);+});+```++The `EventTarget` class is similar in function to `EventEmitter` by evolved+separately as part of browser DOM APIs. The Node.js implementation of+`EventTarget` is an adaptation of the DOM API with a number of Node.js-specific+extensions and behaviors intended to allow it to work more naturally within+the Node.js environment.++### Node.js `EventTarget` vs. DOM `EventTarget`++There are four key differences between the Node.js `EventTarget` and the+[`EventTarget` Web API][]:++1. There is no concept of a hierarchical DOM and event propagation in Node.js.+   That is, an event dispatched to an `EventTarget` does not propagate through+   a hierarchy of nested target objects that may each have their own set of+   handlers for the event.+2. In the Node.js `EventTarget`, any object with a `type` property whose value+   is a string may be dispatched as an `event`.+3. In the Node.js `EventTarget`, if an event listener is an async function+   or returns a `Promise`, and returned `Promise` rejects, the rejection will+   be automatically captured and handled the same way as a listener that throws+   synchronously (see [`EventTarget` Error Handling][] for details).+4. The Node.js `EventTarget` implements an additional set of APIs that allow it+   to closely emulate a subset of the `EventEmitter` API.++### `EventTarget` vs. `EventEmitter`++The Node.js `EventTarget` object implements a modified subset of the+`EventEmitter` API that allows it to closely emulate an `EventEmitter` in+certain situations. It is important to understand, however, that an+`EventTarget` is *not* an instance of `EventEmitter` and cannot be used in+place of an `EventEmitter` in most cases.++1. Unlike `EventEmitter`, any given `listener` can be registered at most once+   per event `type`. Attempts to register a `listener` multiple times will be+   ignored.+2. While the Node.js `EventTarget` exposes an `emit()` function that uses the+   same signature as `EventEmitter`, the event will be dispatched to registered+   listeners as a single event object composed as an array with an additional+   `type` property rather than as separate arguments.+3. The Node.js `EventTarget` does not emulate the full `EventEmitter` API.+   Specifically the `prependListener()`, `prependOnceListener()`,+   `rawListeners()`, `setMaxListeners()`, `getMaxListeners()`, and+   `errorMonitor` APIs are not emulated.+4. The Node.js `EventTarget` does not implement any special default behavior+   for events with type `'error'`.+5. The Node.js `EventTarget` supports `EventListener` objects as well as+   functions as handlers for all event types.++### Event Listener++Event listeners registered for an event `type` may either be JavaScript+functions or objects with a `handleEvent` property whose value is a function.++In either case, the handler function will be invoked with the `event` argument+passed to the `eventTarget.dispatchEvent()` function.++Async functions may be used as event listeners. If an async handler function+rejects, the rejection will be captured and be will handled as described in+[`EventTarget` Error Handling][].++An error thrown by one handler function will not prevent the other handlers+from being invoked.++The return value of a handler function will be ignored.++Handlers are always invoked in the order they were added.++Handler functions may mutate the `event` object.++```js+function handler1(event) {+  console.log(event.type);  // Prints 'foo'+  event.a = 1;+}++async function handler2(event) {+  console.log(event.type);  // Prints 'foo'+  console.log(event.a);  // Prints 1+}++const handler3 = {+  handleEvent(event) {+    console.log(event.type);  // Prints 'foo'+  }+};++const handler4 = {+  async handleEvent(event) {+    console.log(event.type);  // Prints 'foo'+  }+};++const target = new EventTarget();+target.addEventListener('foo', handler1);+target.addEventListener('foo', handler2);+target.addEventListener('foo', handler3);+target.addEventListener('foo', handler4, { once: true });++target.dispatchEvent({ type: 'foo' });+```++When the `eventTarget.emit()` function is used. the arguments passed+following the `type` are composed into a single Array object with an+additional `type` property:++```js+const target = new EventTarget();+target.on('foo', (event) => {+  console.log(event.type);  // Prints 'foo'+  console.log(event[0]);  // Prints 'a'+});+target.emit('foo', 'a');+```++### `EventTarget` Error Handling++When a registered event listener throws (or returns a Promise that rejects),+by default the error will be forwarded to the `process.on('uncaughtException`)+event. Throwing within an event listener will *not* stop the other registered+handlers from being invoked.++Similarly to `EventEmitter`, the `EventTarget.captureRejectionSymbol` property+may be set on the `EventTarget` to override the default error handling and+prevent the `'uncaughtException'` event from being emitted.++```js+const target = new EventTarget();+target[EventTarget.captureRejectionSymbol] = (error, event) => {+  console.log(error.message, event.type);+};+```++This error handling behavior means that errors thrown within an event+listener *do not terminate the Node.js process by default*.++The `EventTarget` does not implement any special default handling for+`'error'` type events.++### Class: `Event`+<!-- YAML+added: REPLACEME+-->++The `Event` object is an adaptation of the [`Event` Web API][].++#### Constructor: `new Event(type[, options])`+<!-- YAML+added: REPLACEME+-->++* `type` {string}+* `options` {Object}+  * `cancelable` {boolean}++#### `event.cancelable`+<!-- YAML+added: REPLACEME+-->++* Type: {boolean}++#### `event.defaultPrevented`+<!-- YAML+added: REPLACEME+-->++* Type: {boolean}++Will be `true` if `cancelable` is `true` and `event.preventDefault()` has been+called.++#### `event.preventDefault()`+<!-- YAML+added: REPLACEME+-->++Sets the `defaultPrevented` property to `true` if `cancelable` is `true`.++#### `event.timestamp`+<!-- YAML+added: REPLACEME+-->++* Type: {number}++The millisecond timestamp when the `Event` object was created.++#### `event.type`+<!-- YAML+added: REPLACEME+-->++* Type: {string}++The event type identifier.++### Class: `EventTarget`+<!-- YAML+added: REPLACEME+-->++#### Constructor: `new EventTarget()`+<!-- YAML+added: REPLACEME+-->++Creates a new `EventTarget` object.++#### `eventTarget.addEventListener(type, listener[, options])`+<!-- YAML+added: REPLACEME+-->++* `type` {string}+* `listener` {Function|EventListener}+* `options` {Object}+  * `once` {boolean}++Adds a new handler for the `type` event. Any given `listener` will be added+only once per `type`.++If the `once` option is `true`, the `listener` will be removed after the+next time a `type` event is dispatched.++#### `eventTarget.addListener(type, listener[, options])`

Given that both of these are functions from existing APIs, there's really nothing we can do to rename them to something less confusing.

jasnell

comment created time in 2 days

push eventjasnell/node

James M Snell

commit sha 9bfecf23ee721dc99cb08b51a4db1962579d5ae0

fixup! [Squash] match dom api behavior

view details

push time in 2 days

Pull request review commentnodejs/node

events: initial implementation of experimental EventTarget

+'use strict';++const {+  ArrayFrom,+  Object,+  Map,+  SafeWeakMap,+  Set,+  SymbolFor,+} = primordials;++const {+  codes: {+    ERR_INVALID_ARG_TYPE,+    ERR_INVALID_THIS,+    ERR_EVENT_RECURSION,+  }+} = require('internal/errors');+const kRejection = SymbolFor('nodejs.rejection');++const perf_hooks = require('perf_hooks');+const { customInspectSymbol } = require('internal/util');+const { inspect } = require('util');++const events = new SafeWeakMap();+const targets = new SafeWeakMap();++class Event {+  constructor(type, options) {+    if (typeof type !== 'string')+      throw new ERR_INVALID_ARG_TYPE('type', 'string', type);

Done :-)

jasnell

comment created time in 2 days

Pull request review commentnodejs/node

events: initial implementation of experimental EventTarget

+'use strict';++const {+  ArrayFrom,+  Object,+  Map,+  SafeWeakMap,+  Set,+  SymbolFor,+} = primordials;++const {+  codes: {+    ERR_INVALID_ARG_TYPE,+    ERR_INVALID_THIS,+    ERR_EVENT_RECURSION,+  }+} = require('internal/errors');+const kRejection = SymbolFor('nodejs.rejection');++const perf_hooks = require('perf_hooks');+const { customInspectSymbol } = require('internal/util');+const { inspect } = require('util');++const events = new SafeWeakMap();+const targets = new SafeWeakMap();++class Event {+  constructor(type, options) {+    if (typeof type !== 'string')+      throw new ERR_INVALID_ARG_TYPE('type', 'string', type);+    if (options != null && typeof options !== 'object')+      throw new ERR_INVALID_ARG_TYPE('options', 'object', options);+    const { cancelable } = { ...options };+    events.set(this, {+      type,+      defaultPrevented: false,+      cancelable: !!cancelable,+      timestamp: perf_hooks.performance.now()+    });+  }++  [customInspectSymbol]() {+    const obj = {+      type: this.type,+      defaultPrevented: this.defaultPrevented,+      cancelable: this.cancelable,+    };+    return `${this.constructor.name} ${inspect(obj)}`;+  }+}++function preventDefault() {+  const state = events.get(this);+  if (state == null) {+    throw new ERR_INVALID_THIS('Event');+  }+  state.defaultPrevented = true;+}++Object.defineProperties(Event.prototype, {+  type: {+    enumerable: true,+    configurable: true,+    get() {+      const props = events.get(this);+      if (props == null) {+        throw new ERR_INVALID_THIS('Event');+      }+      return props.type;+    }+  },+  cancelable: {+    enumerable: true,+    configurable: true,+    get() {+      const props = events.get(this);+      if (props == null) {+        throw new ERR_INVALID_THIS('Event');+      }+      return props.cancelable;+    }+  },+  defaultPrevented: {+    enumerable: true,+    configurable: true,+    get() {+      const props = events.get(this);+      if (props == null) {+        throw new ERR_INVALID_THIS('Event');+      }+      return props.cancelable && props.defaultPrevented;+    }+  },+  timeStamp: {+    enumerable: true,+    configurable: true,+    get() {+      const props = events.get(this);+      if (props == null) {+        throw new ERR_INVALID_THIS('Event');+      }+      return props.timestamp;+    }+  },+  preventDefault: {+    enumerable: true,+    configurable: true,+    writable: true,+    value: preventDefault+  }+});++class EventTarget {+  constructor() {+    targets.set(this, {+      events: new Map(),+      emitting: new Set()+    });+  }++  static captureRejectionSymbol = kRejection;+}++// EventTarget API++function validateType(type) {+  if (typeof type !== 'string')+    throw new ERR_INVALID_ARG_TYPE('type', 'string', type);+}++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 } = options;+  if (typeof once !== 'boolean')+    throw new ERR_INVALID_ARG_TYPE('options.once', 'boolean', once);+  return { once };+}++function addEventListener(type, listener, options = {}) {+  const state = targets.get(this);+  if (state === undefined)+    throw new ERR_INVALID_THIS('EventTarget');++  validateType(type);+  validateListener(listener);++  const { once } = validateEventListenerOptions(options);++  let handlers = state.events.get(type);+  if (handlers === undefined) {+    handlers = new Map();+    state.events.set(type, handlers);+  }++  if (handlers.has(listener))+    return;++  const callback =+    typeof listener === 'function' ?+      listener.bind(this) :+      listener.handleEvent.bind(listener);++  const handler = {+    callback,+    once,+    remove: removeEventListener.bind(this, type, listener)+  };++  handlers.set(listener, handler);+}++function removeEventListener(type, listener) {+  const state = targets.get(this);+  if (state === undefined)+    throw new ERR_INVALID_THIS('EventTarget');++  validateType(type);+  validateListener(listener);++  const handlers = state.events.get(type);+  if (handlers !== undefined) {+    handlers.delete(listener);+    if (handlers.size === 0)+      state.events.delete(type);+  }+}++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) {+  if (typeof that[kRejection] === 'function') {+    that[kRejection](err, event);+  } else {+    process.emit('uncaughtException', err, event);+  }+}++function dispatchEvent(event) {+  const state = targets.get(this);+  if (state === undefined)+    throw new ERR_INVALID_THIS('EventTarget');++  if (event == null ||+      typeof event !== 'object' ||+      typeof event.type !== 'string') {+    throw new ERR_INVALID_ARG_TYPE('event', 'Event', event);+  }++  if (state.emitting.has(event.type)) {+    throw new ERR_EVENT_RECURSION(event.type);+  }++  const handlers = state.events.get(event.type);+  if (handlers !== undefined) {+    state.emitting.add(event.type);+    try {+      const copy = ArrayFrom(handlers.values());+      for (let n = 0; n < copy.length; n++) {+        if (copy[n].once)+          copy[n].remove();+        try {+          const result = copy[n].callback(event);+          if (result !== undefined && result !== null) {+            addCatch(this, result, event);+          }+        } catch (err) {+          emitUnhandledRejectionOrErr(this, err, event);+        }+      }+    } finally {+      state.emitting.delete(event.type);+    }+  }+}++// EventEmitter-ish API:++function eventNames() {+  const state = targets.get(this);+  if (state === undefined)+    throw new ERR_INVALID_THIS('EventTarget');+  return ArrayFrom(state.events.keys());+}++function listenerCount(type) {+  const state = targets.get(this);+  if (state === undefined)+    throw new ERR_INVALID_THIS('EventTarget');+  validateType(type);+  const handlers = state.events.get(type);+  return handlers !== undefined ? handlers.size : 0;+}++function off(type, listener) {+  this.removeEventListener(type, listener);+  return this;+}++function on(type, listener) {+  this.addEventListener(type, listener);+  return this;+}

For this, I'd suspect that anyone using the EventTarget interface is likely going to be most familiar with the register-only-once behavior currently implemented by browsers

jasnell

comment created time in 2 days

push eventjasnell/node

James M Snell

commit sha 90d6b8baaa9dd3308090a8939bec81d34e9fed27

[Squash] Use internal private fields instead

view details

James M Snell

commit sha c215304ef26773990d7098813c519ef2ef5cdcbd

[Squash] match dom api behavior

view details

push time in 2 days

Pull request review commentnodejs/node

events: initial implementation of experimental EventTarget

+'use strict';++const {+  ArrayFrom,+  Object,+  Map,+  SafeWeakMap,+  Set,+  SymbolFor,+} = primordials;++const {+  codes: {+    ERR_INVALID_ARG_TYPE,+    ERR_INVALID_THIS,+    ERR_EVENT_RECURSION,+  }+} = require('internal/errors');+const kRejection = SymbolFor('nodejs.rejection');++const perf_hooks = require('perf_hooks');+const { customInspectSymbol } = require('internal/util');+const { inspect } = require('util');++const events = new SafeWeakMap();+const targets = new SafeWeakMap();++class Event {+  constructor(type, options) {+    if (typeof type !== 'string')+      throw new ERR_INVALID_ARG_TYPE('type', 'string', type);

We could easily copy the browser's behavior here and coerce the type to a string.

jasnell

comment created time in 2 days

Pull request review commentnodejs/node

events: initial implementation of experimental EventTarget

 if the `EventEmitter` emits `'error'`. It removes all listeners when exiting the loop. The `value` returned by each iteration is an array composed of the emitted event arguments. +## `EventTarget` and `Event` API+<!-- YAML+added: REPLACEME+-->++> Stability: 1 - Experimental++The global `EventTarget` and `Event` objects are a Node.js-specific adaptation+of the [`EventTarget` Web API][].++```js+const eventTarget = new EventTarget();+const event = new Event('foo');++eventTarget.addEventListener('foo', (event) => {+  console.log(`The ${event.type} event happened`);+});+```++The `EventTarget` class is similar in function to `EventEmitter` by evolved+separately as part of browser DOM APIs. The Node.js implementation of+`EventTarget` is an adaptation of the DOM API with a number of Node.js-specific+extensions and behaviors intended to allow it to work more naturally within+the Node.js environment.++### Node.js `EventTarget` vs. DOM `EventTarget`++There are four key differences between the Node.js `EventTarget` and the+[`EventTarget` Web API][]:++1. There is no concept of a hierarchical DOM and event propagation in Node.js.+   That is, an event dispatched to an `EventTarget` does not propagate through+   a hierarchy of nested target objects that may each have their own set of+   handlers for the event.+2. In the Node.js `EventTarget`, any object with a `type` property whose value+   is a string may be dispatched as an `event`.+3. In the Node.js `EventTarget`, if an event listener is an async function+   or returns a `Promise`, and returned `Promise` rejects, the rejection will+   be automatically captured and handled the same way as a listener that throws+   synchronously (see [`EventTarget` Error Handling][] for details).+4. The Node.js `EventTarget` implements an additional set of APIs that allow it+   to closely emulate a subset of the `EventEmitter` API.++### `EventTarget` vs. `EventEmitter`++The Node.js `EventTarget` object implements a modified subset of the+`EventEmitter` API that allows it to closely emulate an `EventEmitter` in+certain situations. It is important to understand, however, that an+`EventTarget` is *not* an instance of `EventEmitter` and cannot be used in+place of an `EventEmitter` in most cases.++1. Unlike `EventEmitter`, any given `listener` can be registered at most once+   per event `type`. Attempts to register a `listener` multiple times will be+   ignored.+2. While the Node.js `EventTarget` exposes an `emit()` function that uses the+   same signature as `EventEmitter`, the event will be dispatched to registered+   listeners as a single event object composed as an array with an additional+   `type` property rather than as separate arguments.+3. The Node.js `EventTarget` does not emulate the full `EventEmitter` API.+   Specifically the `prependListener()`, `prependOnceListener()`,+   `rawListeners()`, `setMaxListeners()`, `getMaxListeners()`, and+   `errorMonitor` APIs are not emulated.+4. The Node.js `EventTarget` does not implement any special default behavior+   for events with type `'error'`.+5. The Node.js `EventTarget` supports `EventListener` objects as well as+   functions as handlers for all event types.++### Event Listener++Event listeners registered for an event `type` may either be JavaScript+functions or objects with a `handleEvent` property whose value is a function.++In either case, the handler function will be invoked with the `event` argument+passed to the `eventTarget.dispatchEvent()` function.++Async functions may be used as event listeners. If an async handler function+rejects, the rejection will be captured and be will handled as described in+[`EventTarget` Error Handling][].++An error thrown by one handler function will not prevent the other handlers+from being invoked.++The return value of a handler function will be ignored.++Handlers are always invoked in the order they were added.++Handler functions may mutate the `event` object.++```js+function handler1(event) {+  console.log(event.type);  // Prints 'foo'+  event.a = 1;+}++async function handler2(event) {+  console.log(event.type);  // Prints 'foo'+  console.log(event.a);  // Prints 1+}++const handler3 = {+  handleEvent(event) {+    console.log(event.type);  // Prints 'foo'+  }+};++const handler4 = {+  async handleEvent(event) {+    console.log(event.type);  // Prints 'foo'+  }+};++const target = new EventTarget();+target.addEventListener('foo', handler1);+target.addEventListener('foo', handler2);+target.addEventListener('foo', handler3);+target.addEventListener('foo', handler4, { once: true });++target.dispatchEvent({ type: 'foo' });+```++When the `eventTarget.emit()` function is used. the arguments passed

Yes, this is purely to allow an isomorphic approach with EventEmitter. I consider the emit() here to be optional and I'm fine with omitting it if folks don't think it's necessary.

jasnell

comment created time in 2 days

Pull request review commentnodejs/node

events: initial implementation of experimental EventTarget

 if the `EventEmitter` emits `'error'`. It removes all listeners when exiting the loop. The `value` returned by each iteration is an array composed of the emitted event arguments. +## `EventTarget` and `Event` API+<!-- YAML+added: REPLACEME+-->++> Stability: 1 - Experimental++The global `EventTarget` and `Event` objects are a Node.js-specific adaptation+of the [`EventTarget` Web API][].++```js+const eventTarget = new EventTarget();+const event = new Event('foo');++eventTarget.addEventListener('foo', (event) => {+  console.log(`The ${event.type} event happened`);+});+```++The `EventTarget` class is similar in function to `EventEmitter` by evolved+separately as part of browser DOM APIs. The Node.js implementation of+`EventTarget` is an adaptation of the DOM API with a number of Node.js-specific+extensions and behaviors intended to allow it to work more naturally within+the Node.js environment.++### Node.js `EventTarget` vs. DOM `EventTarget`++There are four key differences between the Node.js `EventTarget` and the+[`EventTarget` Web API][]:++1. There is no concept of a hierarchical DOM and event propagation in Node.js.+   That is, an event dispatched to an `EventTarget` does not propagate through+   a hierarchy of nested target objects that may each have their own set of+   handlers for the event.+2. In the Node.js `EventTarget`, any object with a `type` property whose value+   is a string may be dispatched as an `event`.+3. In the Node.js `EventTarget`, if an event listener is an async function+   or returns a `Promise`, and returned `Promise` rejects, the rejection will+   be automatically captured and handled the same way as a listener that throws+   synchronously (see [`EventTarget` Error Handling][] for details).+4. The Node.js `EventTarget` implements an additional set of APIs that allow it+   to closely emulate a subset of the `EventEmitter` API.++### `EventTarget` vs. `EventEmitter`++The Node.js `EventTarget` object implements a modified subset of the+`EventEmitter` API that allows it to closely emulate an `EventEmitter` in+certain situations. It is important to understand, however, that an+`EventTarget` is *not* an instance of `EventEmitter` and cannot be used in+place of an `EventEmitter` in most cases.++1. Unlike `EventEmitter`, any given `listener` can be registered at most once+   per event `type`. Attempts to register a `listener` multiple times will be+   ignored.+2. While the Node.js `EventTarget` exposes an `emit()` function that uses the+   same signature as `EventEmitter`, the event will be dispatched to registered+   listeners as a single event object composed as an array with an additional+   `type` property rather than as separate arguments.+3. The Node.js `EventTarget` does not emulate the full `EventEmitter` API.+   Specifically the `prependListener()`, `prependOnceListener()`,+   `rawListeners()`, `setMaxListeners()`, `getMaxListeners()`, and+   `errorMonitor` APIs are not emulated.+4. The Node.js `EventTarget` does not implement any special default behavior+   for events with type `'error'`.+5. The Node.js `EventTarget` supports `EventListener` objects as well as+   functions as handlers for all event types.++### Event Listener++Event listeners registered for an event `type` may either be JavaScript+functions or objects with a `handleEvent` property whose value is a function.++In either case, the handler function will be invoked with the `event` argument+passed to the `eventTarget.dispatchEvent()` function.++Async functions may be used as event listeners. If an async handler function+rejects, the rejection will be captured and be will handled as described in+[`EventTarget` Error Handling][].++An error thrown by one handler function will not prevent the other handlers

Well, it propagates to process.on('uncaughtException'). Whether or not that's the best here remains to be determined.

jasnell

comment created time in 2 days

Pull request review commentnodejs/node

events: initial implementation of experimental EventTarget

+'use strict';++const {+  ArrayFrom,+  Object,+  Map,+  SafeWeakMap,+  Set,+  SymbolFor,+} = primordials;++const {+  codes: {+    ERR_INVALID_ARG_TYPE,+    ERR_INVALID_THIS,+    ERR_EVENT_RECURSION,+  }+} = require('internal/errors');+const kRejection = SymbolFor('nodejs.rejection');++const perf_hooks = require('perf_hooks');+const { customInspectSymbol } = require('internal/util');+const { inspect } = require('util');++const events = new SafeWeakMap();+const targets = new SafeWeakMap();

Resolving this as a duplicate of the other comment. Definitely making this change but benchmarking as I go.

jasnell

comment created time in 2 days

Pull request review commentnodejs/node

events: initial implementation of experimental EventTarget

 if the `EventEmitter` emits `'error'`. It removes all listeners when exiting the loop. The `value` returned by each iteration is an array composed of the emitted event arguments. +## `EventTarget` and `Event` API+<!-- YAML+added: REPLACEME+-->++> Stability: 1 - Experimental++The global `EventTarget` and `Event` objects are a Node.js-specific adaptation+of the [`EventTarget` Web API][].++```js+const eventTarget = new EventTarget();+const event = new Event('foo');++eventTarget.addEventListener('foo', (event) => {+  console.log(`The ${event.type} event happened`);+});+```++The `EventTarget` class is similar in function to `EventEmitter` by evolved+separately as part of browser DOM APIs. The Node.js implementation of+`EventTarget` is an adaptation of the DOM API with a number of Node.js-specific+extensions and behaviors intended to allow it to work more naturally within+the Node.js environment.++### Node.js `EventTarget` vs. DOM `EventTarget`++There are four key differences between the Node.js `EventTarget` and the+[`EventTarget` Web API][]:++1. There is no concept of a hierarchical DOM and event propagation in Node.js.+   That is, an event dispatched to an `EventTarget` does not propagate through+   a hierarchy of nested target objects that may each have their own set of+   handlers for the event.+2. In the Node.js `EventTarget`, any object with a `type` property whose value+   is a string may be dispatched as an `event`.+3. In the Node.js `EventTarget`, if an event listener is an async function

Yes, we're being explicit about how we differ from the browser-side API and I'm accounting for things that users will definitely end up doing anyway.

jasnell

comment created time in 2 days

Pull request review commentnodejs/node

events: initial implementation of experimental EventTarget

 if the `EventEmitter` emits `'error'`. It removes all listeners when exiting the loop. The `value` returned by each iteration is an array composed of the emitted event arguments. +## `EventTarget` and `Event` API+<!-- YAML+added: REPLACEME+-->++> Stability: 1 - Experimental++The global `EventTarget` and `Event` objects are a Node.js-specific adaptation

entirely possible if we don't want to allow users to create EventTarget instances. It would just be a matter of not exporting the global

jasnell

comment created time in 2 days

Pull request review commentnodejs/node

events: initial implementation of experimental EventTarget

+'use strict';++const {+  ArrayFrom,+  Object,+  Map,+  SafeWeakMap,+  Set,+  SymbolFor,+} = primordials;++const {+  codes: {+    ERR_INVALID_ARG_TYPE,+    ERR_INVALID_THIS,+    ERR_EVENT_RECURSION,+  }+} = require('internal/errors');+const kRejection = SymbolFor('nodejs.rejection');++const perf_hooks = require('perf_hooks');+const { customInspectSymbol } = require('internal/util');+const { inspect } = require('util');++const events = new SafeWeakMap();+const targets = new SafeWeakMap();

Btw, just benchmarking the two variants (with the WeakMap vs. private properties) and the performance for both are about the same and both completely abysmal compared to EventEmitter ;-) ... I'll be working on improving that next.

jasnell

comment created time in 2 days

Pull request review commentnodejs/node

events: initial implementation of experimental EventTarget

+'use strict';++const {+  ArrayFrom,+  Object,+  Map,+  SafeWeakMap,+  Set,+  SymbolFor,+} = primordials;++const {+  codes: {+    ERR_INVALID_ARG_TYPE,+    ERR_INVALID_THIS,+    ERR_EVENT_RECURSION,+  }+} = require('internal/errors');+const kRejection = SymbolFor('nodejs.rejection');++const perf_hooks = require('perf_hooks');+const { customInspectSymbol } = require('internal/util');+const { inspect } = require('util');++const events = new SafeWeakMap();+const targets = new SafeWeakMap();++class Event {+  constructor(type, options) {+    if (typeof type !== 'string')+      throw new ERR_INVALID_ARG_TYPE('type', 'string', type);+    if (options != null && typeof options !== 'object')+      throw new ERR_INVALID_ARG_TYPE('options', 'object', options);+    const { cancelable } = { ...options };+    events.set(this, {+      type,+      defaultPrevented: false,+      cancelable: !!cancelable,+      timestamp: perf_hooks.performance.now()+    });+  }++  [customInspectSymbol]() {+    const obj = {+      type: this.type,+      defaultPrevented: this.defaultPrevented,+      cancelable: this.cancelable,+    };+    return `${this.constructor.name} ${inspect(obj)}`;+  }+}++function preventDefault() {+  const state = events.get(this);+  if (state == null) {+    throw new ERR_INVALID_THIS('Event');+  }+  state.defaultPrevented = true;+}++Object.defineProperties(Event.prototype, {+  type: {+    enumerable: true,+    configurable: true,+    get() {+      const props = events.get(this);+      if (props == null) {+        throw new ERR_INVALID_THIS('Event');+      }+      return props.type;+    }+  },+  cancelable: {+    enumerable: true,+    configurable: true,+    get() {+      const props = events.get(this);+      if (props == null) {+        throw new ERR_INVALID_THIS('Event');+      }+      return props.cancelable;+    }+  },+  defaultPrevented: {+    enumerable: true,+    configurable: true,+    get() {+      const props = events.get(this);+      if (props == null) {+        throw new ERR_INVALID_THIS('Event');+      }+      return props.cancelable && props.defaultPrevented;+    }+  },+  timeStamp: {+    enumerable: true,+    configurable: true,+    get() {+      const props = events.get(this);+      if (props == null) {+        throw new ERR_INVALID_THIS('Event');+      }+      return props.timestamp;+    }+  },+  preventDefault: {+    enumerable: true,+    configurable: true,+    writable: true,+    value: preventDefault+  }+});++class EventTarget {+  constructor() {+    targets.set(this, {+      events: new Map(),+      emitting: new Set()+    });+  }++  static captureRejectionSymbol = kRejection;+}++// EventTarget API++function validateType(type) {+  if (typeof type !== 'string')+    throw new ERR_INVALID_ARG_TYPE('type', 'string', type);+}++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 } = options;+  if (typeof once !== 'boolean')+    throw new ERR_INVALID_ARG_TYPE('options.once', 'boolean', once);+  return { once };+}++function addEventListener(type, listener, options = {}) {+  const state = targets.get(this);+  if (state === undefined)+    throw new ERR_INVALID_THIS('EventTarget');++  validateType(type);+  validateListener(listener);++  const { once } = validateEventListenerOptions(options);++  let handlers = state.events.get(type);+  if (handlers === undefined) {+    handlers = new Map();+    state.events.set(type, handlers);+  }++  if (handlers.has(listener))+    return;++  const callback =+    typeof listener === 'function' ?+      listener.bind(this) :+      listener.handleEvent.bind(listener);++  const handler = {+    callback,+    once,+    remove: removeEventListener.bind(this, type, listener)+  };++  handlers.set(listener, handler);+}++function removeEventListener(type, listener) {+  const state = targets.get(this);+  if (state === undefined)+    throw new ERR_INVALID_THIS('EventTarget');++  validateType(type);+  validateListener(listener);++  const handlers = state.events.get(type);+  if (handlers !== undefined) {+    handlers.delete(listener);+    if (handlers.size === 0)+      state.events.delete(type);+  }+}++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) {+  if (typeof that[kRejection] === 'function') {+    that[kRejection](err, event);+  } else {+    process.emit('uncaughtException', err, event);+  }+}++function dispatchEvent(event) {+  const state = targets.get(this);+  if (state === undefined)+    throw new ERR_INVALID_THIS('EventTarget');++  if (event == null ||+      typeof event !== 'object' ||+      typeof event.type !== 'string') {+    throw new ERR_INVALID_ARG_TYPE('event', 'Event', event);+  }++  if (state.emitting.has(event.type)) {+    throw new ERR_EVENT_RECURSION(event.type);+  }++  const handlers = state.events.get(event.type);+  if (handlers !== undefined) {+    state.emitting.add(event.type);+    try {+      const copy = ArrayFrom(handlers.values());+      for (let n = 0; n < copy.length; n++) {+        if (copy[n].once)+          copy[n].remove();+        try {+          const result = copy[n].callback(event);+          if (result !== undefined && result !== null) {+            addCatch(this, result, event);+          }+        } catch (err) {+          emitUnhandledRejectionOrErr(this, err, event);+        }+      }+    } finally {+      state.emitting.delete(event.type);+    }+  }+}++// EventEmitter-ish API:++function eventNames() {+  const state = targets.get(this);+  if (state === undefined)+    throw new ERR_INVALID_THIS('EventTarget');+  return ArrayFrom(state.events.keys());+}++function listenerCount(type) {+  const state = targets.get(this);+  if (state === undefined)+    throw new ERR_INVALID_THIS('EventTarget');+  validateType(type);+  const handlers = state.events.get(type);+  return handlers !== undefined ? handlers.size : 0;+}++function off(type, listener) {+  this.removeEventListener(type, listener);+  return this;+}++function on(type, listener) {+  this.addEventListener(type, listener);+  return this;+}

My preference would be to differ in behavior from EventEmitter. Consumers of this API are already going to have to treat it differently from EventEmitter due to the fact that handler functions receive only a single event argument.

jasnell

comment created time in 2 days

Pull request review commentnodejs/node

events: initial implementation of experimental EventTarget

+'use strict';++const {+  ArrayFrom,+  Object,+  Map,+  SafeWeakMap,+  Set,+  SymbolFor,+} = primordials;++const {+  codes: {+    ERR_INVALID_ARG_TYPE,+    ERR_INVALID_THIS,+    ERR_EVENT_RECURSION,+  }+} = require('internal/errors');+const kRejection = SymbolFor('nodejs.rejection');++const perf_hooks = require('perf_hooks');+const { customInspectSymbol } = require('internal/util');+const { inspect } = require('util');++const events = new SafeWeakMap();+const targets = new SafeWeakMap();

Specifically, I'm less concerned right now with the performance as I am with the basic model and behavior.

jasnell

comment created time in 2 days

Pull request review commentnodejs/node

events: initial implementation of experimental EventTarget

+'use strict';++const {+  ArrayFrom,+  Object,+  Map,+  SafeWeakMap,+  Set,+  SymbolFor,+} = primordials;++const {+  codes: {+    ERR_INVALID_ARG_TYPE,+    ERR_INVALID_THIS,+    ERR_EVENT_RECURSION,+  }+} = require('internal/errors');+const kRejection = SymbolFor('nodejs.rejection');++const perf_hooks = require('perf_hooks');+const { customInspectSymbol } = require('internal/util');+const { inspect } = require('util');++const events = new SafeWeakMap();+const targets = new SafeWeakMap();

See: https://github.com/nodejs/node/pull/33556#discussion_r430012404 ;-) ... the short answer is yes, absolutely.

jasnell

comment created time in 2 days

Pull request review commentnodejs/node

events: initial implementation of experimental EventTarget

+'use strict';++const {+  ArrayFrom,+  Object,+  Map,+  SafeWeakMap,+  Set,+  SymbolFor,+} = primordials;++const {+  codes: {+    ERR_INVALID_ARG_TYPE,+    ERR_INVALID_THIS,+    ERR_EVENT_RECURSION,+  }+} = require('internal/errors');+const kRejection = SymbolFor('nodejs.rejection');++const perf_hooks = require('perf_hooks');+const { customInspectSymbol } = require('internal/util');+const { inspect } = require('util');++const events = new SafeWeakMap();+const targets = new SafeWeakMap();

Yep, I have not benchmarked this. I did it this way initially as it matches up with some of the userland implementations out there. I want to add some benchmarks for this and benchmark the different approaches.

jasnell

comment created time in 2 days

Pull request review commentnodejs/node

lib: initial experimental AbortController implementation

+'use strict';++// Modeled very closely on the AbortController implementation+// in https://github.com/mysticatea/abort-controller (MIT license)++const {+  SafeWeakMap,+  Object,+  Symbol,+} = primordials;+const EventEmitter = require('events');+const {+  codes: { ERR_INVALID_THIS }+} = require('internal/errors');++const signals = new SafeWeakMap();+const controllers = new SafeWeakMap();++class AbortSignal extends EventEmitter {

See #33556

jasnell

comment created time in 2 days

pull request commentnodejs/node

lib: initial experimental AbortController implementation

Initial implementation of EventTarget in #33556. Assuming that one goes through, this PR will be modified such that AbortController.prototype.signal is an instance of EventTarget rather than EventEmitter.

jasnell

comment created time in 2 days

PR opened nodejs/node

events: initial implementation of experimental EventTarget

Initial experimental implementation of EventTarget.

This is an adaptation of the Web API EventTarget modified to conform to Node.js' needs. The details are explained in the documentation edits included in this commit.

Refs: https://github.com/nodejs/node/pull/33527

Checklist
  • [x] make -j4 test (UNIX), or vcbuild test (Windows) passes
  • [x] tests and/or benchmarks are included
  • [x] documentation is changed or added
  • [x] commit message follows commit guidelines
+1057 -0

0 comment

10 changed files

pr created time in 2 days

push eventjasnell/node

James M Snell

commit sha 7179ef67991c0e94fd637846af2424566c06d537

events: initial implementation of experimental EventTarget See documentation changes for details Signed-off-by: James M Snell <jasnell@gmail.com>

view details

push time in 2 days

create barnchjasnell/node

branch : event-target

created branch time in 2 days

Pull request review commentnodejs/node

lib: initial experimental AbortController implementation

+'use strict';++// Modeled very closely on the AbortController implementation+// in https://github.com/mysticatea/abort-controller (MIT license)++const {+  SafeWeakMap,+  Object,+  Symbol,+} = primordials;+const EventEmitter = require('events');+const {+  codes: { ERR_INVALID_THIS }+} = require('internal/errors');++const signals = new SafeWeakMap();+const controllers = new SafeWeakMap();++class AbortSignal extends EventEmitter {

Yep. The biggest complexity we have on it really is making it somewhat compatible with EventEmitter

jasnell

comment created time in 3 days

issue commentnodejs/node

🚀 AbortController in Node task list

Supporting on callback versions is certainly possible and would likely help make things more consistent.

benjamingr

comment created time in 4 days

more