RFC: `EventEmitter` bindings and `HTTP2` bindings

TLDR;

  • I propose dropping the onEventName-style event handling in Node library bindings in favor of using eventNameHandle-style event handling (covered below)

  • I’m announcing bindings to the http2 NPM module, whichs build on top of the above idea.

onEventName vs eventNameHandle

Many of the types we use in Node extend EventEmitter and operate via callbacks: Stream, ChildProcess, HttpServer, etc. The way we typically write FFI for these events is solely via the on function.


-- PureScript code

onEventName

:: EventEmitterType

-> (Maybe Error -> String -> Effect Unit)

-> Effect Unit

onEventName v cb = runEffectFn2 onEventNameImpl v $ mkEffectFn2 \err s ->

cb (toMaybe err) s

foreign import onEventNameImpl :: EffectFn2 (EventEmitterType) (EffectFn2 (Nullable Error) String Unit) (Unit)


// JavaScript code

export const onEventName = (value, cb) => value.on("eventName", cb);

This approach comes with two downsides that affect all current Node bindings

  • we prevent users from removing these callback at a later time, either after their first call (e.g. once) or programmatically (e.g. off)

  • we force users to add listeners to the end of the underlying listeners array

In purescript-node-event-emitter, I wrote bindings that allow library authors to provide an idiomatic PureScript interface to the underlying JavaScript callback while allowing end-users to decide which callback-adding function to use: on, once, prependListener, or prependOnceListener. Moreover, end-users can remove added listeners via the subscribe variants. Since these callbacks tend to follow a pattern, I defined some utility types like EventHandle2

Here are some examples of this approach:

I propose updating all Node libraries to use the eventHandle API for handling events. This would be a breaking change (albeit a small one) across all Node libraries.

For those using the Node.js backend to build things, would this change be desired?

HTTP2 bindings

I’ve also written bindings to the the http2 module (excluding only the Compatibility API) based on the above eventNameHandle-style. The purescript-node-http2 repo (not yet published) uses custom forks of node-streams, node-net, and bindings I wrote for node-tls that all use the eventNameHandle-style above. While the tests for the http2 repo are still lacking, the bindings should otherwise be sound.

Any feedback on the http2 bindings and my above proposal would be appreciated.

3 Likes

Hey Jordan,

very cool proposal, I like the flexibility it affords to call off and once on the same handle etc.
Here are a few thoughts:

  1. One pattern I’ve seen a lot and quite liked in the TS/JS world was that an onXYZ would actually return an Effect (Effect Unit) where the inner Effect Unit unregisters the listener. The nice thing here in PureScript is that you get a warning if you ignore it. However, for low-level bindings it’s probably nice to expose the full flexibility of the real API, just something to think about.
  2. And of course the most important part:

    I think it reads much nicer to see server # on close. I’d relegate the Handle to the type or the qualified import.
1 Like

Thanks for the feedback. I’ll respond to each below:

an onXYZ would actually return an Effect (Effect Unit) where the inner Effect Unit unregisters the listener. The nice thing here in PureScript is that you get a warning if you ignore it.

That’s what the subscribe functions are for. But if you know that you’ll just write:

void $ subscribe ...

then why not just use on?

drop the handle

I’d love to, but that runs into two problems:

  1. Name conflicts with keyword. Dropping handle from dataHandle is invalid because data is a keyword. I’d like to keep a consistent naming convention so people can skim through docs and immediately know whether something is an event handler or a method, which leads to my other point.
  2. Name conflicts with methods. Dropping handle from closeHandle conflicts with the close method. The only workaround to this is to define all events in their own module and then reference them via ServerEvents.close. Possible, but since there’s already a lot of similarly-named aliases for modules in the http2 test (e.g. Server, NServer), I’d prefer not to use yet another module alias for that.

The best I can think of is to shorten the suffix from Handle to just H. Then you’d write:

server # on closeH do
  ...
2 Likes

Very cool, I missed the subscribe, sorry.
Good points about the Handle, I’m convinced now.
Then from my point of view this sounds very nice, maybe we can sleep over the name again (somehow I feel something like Event instead of handle would be more beginner-friendly, but I’m sure I’d get used to Handle :slight_smile: )

1 Like

I feel something like Event instead of handle would be more beginner-friendly, but I’m sure I’d get used to Handle

I thought about using Event, but then I thought that an event can be both emitted and handled. Right now, the bindings allow one to handle such an event, but they don’t have a nice interface for emitting one (e.g. emitting an event from PS to be consumed by JS). If that was supported, would Event refer only to the emitted event or the handled one? That’s why I went with Handle, so that some future Emit interface could be defined without a breaking change.

2 Likes

Great job with purescript-node-event-emitter - Pursuit , we’ve badly needed this for a long time.

2 Likes

May I suggest moving the emitter argument to the last place, so that it’s emitter # on closeHandle \foo -> bar instead of on closeHandle emitter \foo -> bar?

3 Likes

@fsoikin Ah, good point. I’ll make that change. Thanks.

@jamesbrock Just FYI. The (as yet unreleased) version of event-emitter used in the http2 library removes the *Via functions. The version linked by you above includes those. Just wanted to point that out.

3 Likes

v3.0.0 has been released. See the release notes: Releases · purescript-node/purescript-node-event-emitter · GitHub

Pursuit docs here: Node.EventEmitter - purescript-node-event-emitter - Pursuit

1 Like

I am trying to understand the EventEmitter and EventTarget and NodeEventTarget APIs Events | Node.js v20.3.1 Documentation , and which event API can or must be used in which situations.

The timeline here seems to be that

  1. Node.js published the EventEmitter API.
  2. WHATWG published the EventTarget API.
  3. Node.js published bindings to EventTarget, and also NodeEventTarget which is some kind of emulation wrapper for both EventEmitter and EventTarget?

Any thoughts about this?

The Node docs can be rendered as one page via the Options menu. After doing that, I searched for the following:

  • NodeEventTarget - doesn’t seem like anything builds on top of this…?
  • EventTarget - BroadcastChannel and MessagePort extend this class.

So, I’d guess only those Worker-related things depend on that API?