Skip to content

feat: add emitterFactory and friends #342

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Sep 25, 2020

Conversation

lance
Copy link
Member

@lance lance commented Sep 23, 2020

This commit adds an emitterFactory() function that returns an EmitterFunction. The EmitterFunction may be used to emit events over a supported network transport layer. Currently, only HTTP is supported.

Parameters provided to the emitterFactory() function are the transport Binding (only HTTP supported), the encoding mode (Mode.BINARY or Mode.STRUCTURED), and a TransportFunction. A TransportFunction is a user supplied typed function that takes a Message and Options, and sends the event.

The implementation for emitBinary and emitStructured has been replaced with this simple pattern and those two functions have been removed.

Example:

// The endpoint URL that will receive the event
const sink = 'https://my-event-sink';

// A TransportFunction that uses Axios to send a message over HTTP
function axiosEmitter(message: Message, options?: Options): Promise<unknown> {
  return axios.post(sink, message.body, { headers: message.headers, ...options });
}

// Create an event emitter
const emit = emitterFactory(HTTP, Mode.BINARY, axiosEmitter);

// Emit an event, sending it to the endpoint URL
emit(new CloudEvent{ source: '/example', type: 'example' });

Per @grant

in the PR description, can you write up the before and after with these changes?

BEFORE
This is what we have today.

const emitter = new Emitter({ url: sinkUrl, protocol: Protocol.HTTPBinary });
emitter.send(event);

AFTER
The BEFORE functionality still exists until the release of 4.x at which time it will be removed. The proposed alternative below provides similar capability without an explicit dependency on a transport framework (e.g. axios).

# This function is user supplied and allows the use of axios, superagent, got, etc
# whatever the user is comfortable with
function axiosEmitter(message: Message, options?: Options): Promise<unknown> {
  return axios.post(sink, message.body, { headers: message.headers, ...options });
}

const emit = emitterFactory(HTTP, Mode.BINARY, axiosEmitter);
emit(new CloudEvent{ source: '/example', type: 'example' });

Related: #314

This commit adds an emitterFactory function that returns an EmitterFunction
object. The EmitterFunction may be used to emit events over a supported
network transport layer. Currently, only HTTP is supported.

Parameters provided to the emitterFactory are the transport Binding (only
HTTP supported), the encoding mode (Mode.BINARY or Mode.STRUCTURED), and
a TransportFunction.

The implementation for emitBinary and emitStructured has been replaced
with this simple pattern and those two functions have been removed.

Example:

```js
// The endpoint URL that will receive the event
const sink = 'https://my-event-sink';

// A function that uses Axios to send a message over HTTP
function axiosEmitter(message: Message, options?: Options): Promise<unknown> {
  return axios.post(sink, message.body, { headers: message.headers, ...options });
}

// Create an event emitter
const emit = emitterFactory(HTTP, Mode.BINARY, axiosEmitter);

// Emit an event, sending it to the endpoint URL
emit(new CloudEvent{ source: '/example', type: 'example' });
```

Signed-off-by: Lance Ball <[email protected]>
@lance lance added type/enhancement New feature or request module/transport/http Issues related to the HTTP transport protocol implementation module/lib Related to the main source code version/4.x Issues related to the 4.0 release of this library labels Sep 23, 2020
@lance lance requested a review from a team September 23, 2020 19:34
@lance lance self-assigned this Sep 23, 2020
@lance
Copy link
Member Author

lance commented Sep 23, 2020

Note: added tests include testing for Axios, SuperAgent and Got as HTTP transport libraries.

@lholmquist
Copy link
Contributor

@lance Does it make sense to only have 1 parameter(an option object) passed to the emitterFactory? Then we could easily default values.

In node core, we've done that with some functions.

@lance
Copy link
Member Author

lance commented Sep 24, 2020

@lance Does it make sense to only have 1 parameter(an option object) passed to the emitterFactory? Then we could easily default values.

@lholmquist
I could make that change. Something like this?

const emit = emitterFactory({ binding: HTTP, mode: Mode.BINARY, transport: axiosEmitter });

With binding and mode defaulting to HTTP and Mode.BINARY respectively, the above is equivalent to:

const emit = emitterFactory( { transport: axiosEmitter } );

Personally, I think I'd prefer two parameters: the function and an options object. E.g.

const emit = emitterFactory( axiosEmitter, { binding: HTTP, mode: Mode.BINARY });

Which makes the default call a little cleaner.

const emit = emitterFactory( axiosEmitter );

@grant
Copy link
Member

grant commented Sep 24, 2020

I can appreciate some of the usefulness for this utility, but I don't think a stateful emitter factory should be part of the SDK.


It's really hard to discuss design decisions in the abstract, so in the PR description, can you write up the before and after with these changes? I think I can suggest a different way to achieve the same result without a factory.


I can imagine a developer writing their own factory pattern that is even more flexible than the interface here. For example, a user could just write a wrapper for the emitter function and add extra params.

In terms of interface design, Factories are more common in Java and less so in Node.

Related: #314 – I really think we can allow the factory pattern wrapping an emitter.

@lance
Copy link
Member Author

lance commented Sep 24, 2020

@grant the purpose of this PR is to provide a mechanism in 4.x for users to send events across the wire without this module having an explicit dependency on axios (or any other transport module). The Emitter capabilities as they are now with an explicit dependency on axios will go away. But it would still be nice to provide a simple mechanism for users to easily send events.

I can appreciate some of the usefulness for this utility, but I don't think a stateful emitter factory should be part of the SDK.

The factory is not stateful. But maybe you mean the emitter itself. As I see it, if we want the user to not have to provide the transport binding and serialization mode with every call there must be some state. Otherwise, with each function call that sends an event, the user would be required pass HTTP and Mode.BINARY (or Mode.STRUCTURED) in addition to the event.

It's really hard to discuss design decisions in the abstract, so in the PR description, can you write up the before and after with these changes?

Done.

I think I can suggest a different way to achieve the same result without a factory.

OK - let's hear it.

I can imagine a developer writing their own factory pattern that is even more flexible than the interface here. For example, a user could just write a wrapper for the emitter function and add extra params.

The whole point is that the Emitter as it is today is going away.

In terms of interface design, Factories are more common in Java and less so in Node.

Ahh but using closures to maintain state is quite a popular JavaScript style. I thought you'd like it. How about I rename it to emitterFor( fn: TransportFunction, options: Options ) : EmitterFunction? Then we won't think of it as a factory.

@grant
Copy link
Member

grant commented Sep 24, 2020

I'd rather expose just 1 interface for users to emit CloudEvents. An emitter factory can be a fairly simple sample rather than in the SDK. It's a lot more customizable that way.

User-defined code. Example:

// User-defined emitter class. Not in the SDK
export emitterFactory(options) {
  return {
    send: (ce, customOptions) => {
      const combinedOptions = {...options, ...customOptions};
      Emitter.send(ce, combinedOptions);
    }
  };
}

Ideally it's pretty simple to emit a CloudEvent. Rough interface (you can extend the transport, mode, etc.):

// Static emitter
Emitter.send(ce, "https://cloudevents.io/example");

// Or another style (pseduocode)... ideally only one of these is implemented, this style is more functional. Above is more object oriented.
import {emit} from 'cloudevents/transports/http';
emit(ce, options);

@lance
Copy link
Member Author

lance commented Sep 24, 2020

I'd rather expose just 1 interface for users to emit CloudEvents

I'm not proposing more than 1 interface. As I have said multiple times, Emitter is meant to be removed in v4.0.0. I am proposing a replacement that does not have a dependency on a 3rd party transport library such as axios.

Emitter.send(ce, "https://cloudevents.io/example");

This means that we would not be removing the dependency on axios. Is that what you are suggesting? I understood that removing this dependency is a goal for the v4.0.0 release.

@grant
Copy link
Member

grant commented Sep 24, 2020

This means that we would not be removing the dependency on axios. Is that what you are suggesting? I understood that removing this dependency is a goal for the v4.0.0 release.

No. Removing that dep on a transport library is great. I wasn't complete with the samples.

I guess why do we have a custom emitter at all? We should let users BYO / use your own libraries for emitting using raw primitives of HTTP (headers and body).

Because we're removing the transport library, having a param inserting a transport library seems confusing. It seems like an extra interface that we don't really need.

A user can just use transport library as they want. Example:

const axios = require('axios').default;

const ce = { ... }
const [headers, body] = CloudEvents.asHttpRequest(ce);

axios({
  method: 'post',
  url: '...',
  data: body,
  headers,
});

Creating a wrapper emitter doesn't seem super useful, given there are dozens of transport libraries.

Some transports don't have binary events for example, so I don't know why we are adding binaryEmitter.

Hope you get my general perspective.

@lance
Copy link
Member Author

lance commented Sep 25, 2020

I guess why do we have a custom emitter at all?

I'm not sure what you mean by "custom emitter". This PR is about removing opinionated choices like the use of axios to send messages across the wire, and replacing it with something that is less "custom".

We should let users BYO / use your own libraries for emitting using raw primitives of HTTP (headers and body).

That is already possible. Perhaps you don't recall some of the conversations around this, or are not familiar with the codebase. It landed in this PR. Your hypothetical is available today, it just looks a little different.

const axios = require('axios').default;

const ce = { ... }
const message = HTTP.binary(ce); // Or HTTP.structured(ce)

axios({
  method: 'post',
  url: '...',
  data: message.body,
  headers: message.headers,
});

This PR is about providing users a way of encapsulating behavior. Imagine the user is not using axios, but instead their application has chosen superagent. Sending a Message looks a little different, and it's just clunkier.

import request from "superagent";

const ce = { ... }
const message = HTTP.binary(ce); // Or HTTP.structured(ce)

const post = request.post('...');
for (const key of Object.getOwnPropertyNames(message.headers)) {
  post.set(key, message.headers[key]);
}
post.send(message.body);

This PR would make that look a little different.

// Same code as above but wrapped in a function
// User code - a TransportFunction
function sendViaSuperagent(message) {
  const post = request.post('...');
  for (const key of Object.getOwnPropertyNames(message.headers)) {
    post.set(key, message.headers[key]);
  }
  post.send(message.body);
}

// The returned EmitterFunction handles converting from CloudEvent to Message and invokes 'sendViaSuperagent`
const emit = emitterFor(sendViaSuperagent);

const ce = { ... }
emit(ce);

This encourages encapsulation. For example, HTTP.binary(ce) is only called in one place in the code - in the EmitterFunction. Any specific properties or other attributes that a user may need to provide to axios, superagent or got or whatever is being used are all encapsulated in this one function.

Additionally, and probably more important, we have talked about adding additional transport modes support soon. This would mean that, for example, MQTT would look similar.

// This is user code - a TransportFunction
function sendMQTT(message) {
  // do whatever you need to do with message.body and message.headers to send it over MQTT
}

// Create an EmitterFunction
const emit = emitterFor(sendMQTT, { binding: MQTT });

const ce = { ... };
emit(ce);

Note that this looks exactly the same as sending an event over HTTP. That's by design. In fact, I can use the EmitterFunction interface in my shiny new TypeScript application for CloudEvents. Imagine that you want to build an app that allows devs to provide a EmitterFunction so that it can emit events on their behalf. This makes that possible, and it won't matter what they are doing to actually send the event across the wire - HTTP/MQTT/ProtoBuff/whatever.

Sure, it's a small benefit to the user to provide this. But I don't understand your hostility towards it, especially given that we can already do what you were asking for in the very trivial case of just sending an event with axios.

Copy link
Member

@grant grant left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey Lance, I hope you're not taking my comments as being hostile, it seems useful, I'm just trying to understand why the functionality has to live here (and should other languages have this feature).

Definitely not requesting changes. Thanks for the detailed explanations.

@lholmquist
Copy link
Contributor

Personally, I think I'd prefer two parameters: the function and an options object. E.g.

That works.

@lance
Copy link
Member Author

lance commented Sep 25, 2020

Definitely not requesting changes. Thanks for the detailed explanations.

No worries. I should have been more explicit in the PR description.

@lance
Copy link
Member Author

lance commented Sep 25, 2020

I have incorporated feedback by renaming the function and changing up the parameters.

function emitterFor(fn: TransportFunction, options = { binding: HTTP, mode: Mode.BINARY }): EmitterFunction

PTAL

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
module/lib Related to the main source code module/transport/http Issues related to the HTTP transport protocol implementation type/enhancement New feature or request version/4.x Issues related to the 4.0 release of this library
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants