-
-
Notifications
You must be signed in to change notification settings - Fork 18
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
Alternative SSE client #25
Comments
Just my 2c's as far as I understand this.
So in short, these look like good improvements. |
Thank you for the feedback! As an observation, unsubscribing is not actually required for the correctness of the program. It is there to enable the user to stop receiving events on the given subscription. This reinforces that the API presents a set of unnecessary confusion points. With regards to adding event handlers on the fly, this could still be achieved with some userland code. One possible solution could be something like this: type handlerRegistration {
eventType string
handler func(sse.Event)
}
handlers := map[string]func(sse.Event){} // the key is the event type
handlersCh := make(chan handlerRegistration)
ch := make(chan sse.Event)
go func() {
for {
select {
case e := <-ch:
if h := handlers[e.Type]; h != nil {
h(e)
}
case r := <-handlersCh:
handlers[r.eventType] = r.handler
case <-ctx.Done():
return
}
}
}()
// the call to sse.Connect This is actually very similar to how it's implemented internally. The thing is this code would reside in the user's codebase, which means it can be adjusted to whatever their needs are – subsequent event handler registration could be implemented in a totally different manner, if that's more fit. There could maybe be some type which would wrap the events channel and provide the handler registration functionality separately but honestly for the reasons above I'd rather just not have To collect more data points, what's a concrete scenario where adding handlers after connecting was useful or necessary? |
Thanks for the example. It makes a lot of sense. No concerns there. The use-case for adding handlers after connecting is when the connection is required as part of initialization of a service so the service can receive async responses. This also makes it easy to use different transports for testing or based on configuration. If the client is an end-user (web browser) then it would be the other way around as the application starts before the connection is established or re-established. This isn't a deal breaker as it is easy enough to create a helper that is added as SSE listener before the connection is established and passed to the service after the connection is successful. Just a bit more work. Not a big deal. There are many ways to cut this apple. :) |
Question: where does specifying a custom One thing that is common throughout other codebases is using a subset of an
Unless go-sse needs a full |
@slimsag the When it comes to mocking the client, I think the canonical way to do it in Go is to actually use a mock I also wouldn't add another API entity just for mocking when the same can already be achieved with the standard library directly, in order to keep With all these being said, it will |
I actually don't mean for mocking (although it is useful for that, too) - I really mean for using other HTTP clients (typically a wrapper of an |
I understand now. I actually happen not to be very familiar with this pattern – are there some open-source projects which employ it or some libraries that help with creating such clients? I'm trying to determine whether there is something either unachievable through To me at a first glance, if one wishes to log stuff around a request, a wrapper |
Here are some examples of other projects that do it:
Anywho, it's totally just a suggestion/idea for compatibility with existing code. I think there are ways to workaround it. Feel free to disregard the suggestion :) |
Thank you for the plethora of examples! I find it interesting how in some of them, namely Sourcegraph, Cilium, the implementors of that interface in the respective files actually wrap an I'll most certainly revisit this once I start working on the client. If other people see this discussion and would be interested in having this, please leave a reaction to any of the relevant messages. |
Appendix 1: connection state handlingAs discussed in #37, right now the only way to determine that the client has connected to the server is by having the server send an event after the connection is established. This should not be necessary, though – the browser's Based on its specification something similar could be implemented in Go. Here's how it could look like together with the new client proposal: ctx, cancel := context.WithCancel(context.Background())
defer cancel()
events := make(chan sse.Event)
status := make(chan sse.ConnectStatus)
go func() {
for {
select {
case s := <-status:
/* do something with the status */
case e, ok := <-events:
if !ok { return }
/* consume event */
}
}
}()
config := &see.ConnectConfig{
Status: status,
/* other options */
}
r, _ := http.NewRequestWithContext(ctx, http.MethodGet, "http://localhost:8000", http.NoBody)
if err := sse.Connect(r, events, config); err != nil {
fmt.Fprintln(os.Stderr, err)
} In other words, the new client could take through the
sse.Connect(r, events,
sse.HTTPClient(...),
&sse.Backoff{...},
sse.Buffer(...), // corresponding to the current sse.Connection.Buffer
sse.Status(status)) This would massively bloat the library and would also ask for lengthy names (in order to keep it in a top-level
// valid usages of sse.Connect
sse.Connect(r, events, nil, nil)
sse.Connect(r, events, nil, config)
sse.Connect(r, events, status, config) As seen above, usage without status announcement would be achieved by simply passing a
type ConnectResponse struct {
Status ConnectStatus
Event Event
} I do not like this because it is a discriminated union in disguise, for which Go doesn't have any syntactic support – correct usage is not intuitive but must be documented. It also adds a new top-level type, which means API surface bloat, and bloats usage which doesn't need to check the connection status. Usage aside, here's what type ConnectStatus struct {
Kind ConnectStatusKind
RetryError error
}
type ConnectStatusKind int
const (
ConnectStatusKindConnecting ConnectStatusKind = iota
ConnectStatusKindOpen
ConnectStatusKindClosed
) Not having be it just an enum type makes it more useful – through the The sequence in which status updates would be sent would be the following:
A This would be one way of handling connection state announcement. Let me know what you think! |
Appendix 2:
|
The current client API offers a great deal of flexibility:
sse.Client
multipleConnection
s with the same configuration can be madeThis doesn't come for free, though: both the user-facing API and the implementation code are complex, and the client uses a bit more resources, generates more garbage and must ensure serialized concurrent access to internal state. Moreover, the current client:
As of now, the instantiation of a connection with cancellation, some custom configuration and sending data on a channel looks as follows:
I've added the channel because from what I have observed, most users of the library create callbacks which mainly send the events on a channel to be consumed elsewhere.
I think this is quite a mouthful and I wonder whether enough use of the aforementioned flexibilities is made for them to justify the current API.
Here's another way in which I think the client could be designed. Instead of having a
Client
type and aConnection
type with many methods, we could instead have the following:Usage would look as follows:
It is not that much shorter, but assuming that the context comes from elsewhere and that the configuration is already defined, the code necessary for establishing a connection is significantly shorter – creating an
http.Request
and callingConnect
. Connection with the default configuration would also not need a separate top-level function – just passnil
instead of aConnectionConfig
!There are two important changes here, though:
For example, if we receive three diferent event types and we handle them differently, previously one could do:
With this change, it would look like this:
On the flipside, simple requests would be easier to make. Consider a request to ChatGPT:
This would be the new version:
There are obvious benefits:
NewConnection
->SubscribeMessages
->Connect
As an analogy, imagine if the
net/http.Client
would be used something like this:It would be painful to use.
The main advantage of the new API would be, I believe, that the control of the response is fully in the library user's hands. There are no callbacks one needs to reason about; there is no need for the user to look up the source code to find out how the
Connection
behaves in various respects – for example, in what order the event listeners are called; finally, in a paradoxical manner there would be one single way to do things: for example, if one wants to handle multiple event types, currently they can register multiple callbacks for each event, or write the sameswitch
code as above inside a callback passed toSubscribeAll
. Also, it would be much easier to maintain – this change would result in ~200LOC and 6 public API entities being removed. This reduction in API surface reduces documentation and in the end how much the user must learn about the library in order to use it effectively.Looking forward on your input regarding this API change!
The text was updated successfully, but these errors were encountered: