node-addon-api icon indicating copy to clipboard operation
node-addon-api copied to clipboard

Wrapping Event in a C++ class so that every where you pass an event it works.

Open greggman opened this issue 7 months ago • 8 comments

in JavaScript I can do this

class MyEvent extends Event {
  constructor(type, msg) {
    super(type);
    this.message = msg;
  }
}
const target = new EventTarget();
const e = await new Promise(resolve => {
  target.addEventListener('custom', resolve);
  target.dispatchEvent(new MyEvent('custom', 'hello'));
});
assert.ok(e.message === 'hello');

Is it possible to have MyEvent be a C++ class?

In my current attempt, when I call target.dispatchEvent I get

node:internal/event_target:220
      throw new ERR_INVALID_THIS('Event');
            ^

TypeError [ERR_INVALID_THIS]: Value of "this" must be of type Event
    at get type [as type] (node:internal/event_target:220:13)
    at EventTarget.dispatchEvent (node:internal/event_target:755:40)

I tried setting the prototype chain in C++ and this passes

assert.ok(new MyEvent('custom', 'hello') instanceof Event);

but of course it's not actually an Event, it's a non-event who's prototype chain contains Event which is what node is complaining about.

greggman avatar May 13 '25 01:05 greggman

wrt, I'm trying to verify if this is possible or not. My impression is it's not possible and that I have to do something like create class MyEvent extends Event in JavaScript and then use it from my addon.

greggman avatar May 15 '25 00:05 greggman

Just to make it clear what my goal is here. I'm trying to get dawn.node to be spec compliant.

Dawn is Chromium's implementation of the WebGPU Spec and dawn.node is trying to bring the same API to node.js

That spec has a few requirements of extending existing classes

For example GPUDevice is supposed to extend EventTarget

[Exposed=(Window, Worker), SecureContext interface GPUDevice : EventTarget { ... }

This means, for example, if you patch dispatchEvent on EventTarget and call dispatchEvent on a GPUDevice it must go through the patched function

EventTarget.prototype.dispatchEvent = (function(origFn) {
  return function(event) {
    console.log('type:', event.type);
    return origFn.call(this, event);
  };
})(EventTarget.prototype.dispatchEvent);

someGPUDevice.dispatchEvent(new Event('foobar'));  // should see type: foobar in console

Similarly, as mentioned above, a custom event WebGPU event, implemented by dawn.node, needs to act like an Event. In particular GPUUncapturedErrorEvent extends Event

Exposed=(Window, Worker), SecureContext]]
interface GPUUncapturedErrorEvent : Event {
   constructor(
       DOMString type,
       GPUUncapturedErrorEventInit gpuUncapturedErrorEventInitDict
   );
   [SameObject] readonly attribute GPUError error;
};

dictionary GPUUncapturedErrorEventInit : EventInit {
   required GPUError error;
};

This means for example, it's valid to dispatchEvent an GPUncaptureErrorEvent through an EventTarget

const target = new EventTarget();
target.dispatchEvent(new GPUUncapturedErrorEvent('uncapturederror', {
  error: new GPUValidationError('msg'),
});

Which is the issue above.

Several other requirements

GPUDevice.prototype instanceof EventTarget  // should be true
someGPUDevice instanceof EventTarget        // should be true
someGPUDevice instanceof GPUDevice          // should be true

I feel like I'm reading around the net and in issues in this repo that NAPI doesn't support what I'm trying to do and basically I'm trying to verify that.

greggman avatar May 15 '25 07:05 greggman

Node-API does not currently provide a mechanism to create a C++ class that extends a JavaScript class. There's some technical limitations (as a class in V8 is a FunctionTemplate, which does not have a corresponding Node-API counterpart).

Have you tried prototype manipulation, eg. with Object.setPrototypeOf()? I am not sure if this would work, but if you have time to try it out... 👍 But it may not succeed in your requirement of someGPUDevice instanceof GPUDevice === true 🤷

Perhaps look at https://github.com/nodejs/node-addon-api/issues/229 for some other discussions regarding same.

Let us know how it turns out!

KevinEady avatar May 19 '25 15:05 KevinEady

So I did a bunch of hacking to get things to kind of work. The frustrating part is this works in the browsers (chrome/firefox/safari) that implement WebGPU but apparently with the current Napi design, and because limitations in v8 if I understand correctly, it's not possible in node addons.

My workarounds involved using a JavaScript object instead of C++ object for GPUUncapturedErrorEvent which works because GPUUncapturedErrorEvent is basically just a vehicle for some data. No functionality. I had to do similar things for GPUPipelineError as it's supposed to extend DOMException.

On the other hand, I could not workaround the fact that GPUDevice is supposed to extend EventTarget. That said, I know of no apis that take an EventTarget so it's unlikely to mater.

greggman avatar Jun 07 '25 00:06 greggman

@greggman I just realized that we can utilize a trick in JS to allow extending a JS class in node-addon-api, working around the limitation.

This should be already doable in C node-api. https://github.com/nodejs/node-addon-api/pull/1670 is a work to allow it in node-addon-api.

legendecas avatar Jun 19 '25 15:06 legendecas

This issue is stale because it has been open many days with no activity. It will be closed soon unless the stale label is removed or a comment is made.

github-actions[bot] avatar Oct 02 '25 00:10 github-actions[bot]

class MyEvent extends Event { constructor(type, msg) { super(type); this.message = msg; } } const target = new EventTarget(); const e = await new Promise(resolve => { target.addEventListener('custom', resolve); target.dispatchEvent(new MyEvent('custom', 'hello')); }); assert.ok(e.message === 'hello');

jackdenied avatar Nov 04 '25 14:11 jackdenied

@jackdenied , not sure the point of your reply. This post is about C++, not JavaScript

greggman avatar Nov 04 '25 22:11 greggman