Open NathanBaulch opened 3 months ago
Hi Nathan,
Thank you for raising this issue! Apologies for the delayed response – life just seems to happen and hamper my swiftness to reply.
As a side-note, in retrospective it's just funny to me that I didn't think of these use-cases when I've first designed the library:
It can only make me happy that there's enough usage for them to have been uncovered and that even without their support the library has still gained some traction.
Coming back on track to the issue, I can imagine how partitioning users by permissions might not work. But some other idea to use topics just came to my mind and maybe could work – let me know if it could apply to your use-case. Here's how it goes:
<entity>[:<scopes>]:<operation>
posts:<group-id>:view
or posts:<group-id>:create
posts:...:create
creating a post would generate an event for those with posts:...:view
)comments:<post-id>:view
?) map[TenantID]*sse.Joe
) and a custom HTTP handler to subscribe new users to the correct provider.If your scenario is one where such an implementation would work, you're in the happy case to not have to wait for a library update.
If you're not, let's see what could be done. I'll go through your proposals and also add some of mine:
map[string]any
field to Message", "Pass context.Context
to MessageWriter.Send
so I can grab metadata from that"
These are very similar, in fact almost the same. I'd like to understand better what sort of data you would attach? I assume you'd use this additional data in conjunction with a custom sse.MessageWriter
implementation (maybe a wrapper around sse.Session
?) which contains some extra data about the user, and based on these two data sets (message + user) you'd do the filtering.
MessageWriter
should not cause performance issues – if the Send
implementation uses sse.Message.WriteTo
the performance won't be harmed. In fact, as of now there isn't any way to write an sse.Message
other than the optimized one. Why do you say you'd have to unmarshal the Message
every time? Do you require reading the message data for authorization? Wouldn't you be able to use the sse.Message.Event
field and filter based on that? For example, it could have the format post.<group-id>.created
, and you could dispatch it to users which have the posts:view
permission in the respective group.MessageWriter
? Maybe a brief API outline would help so I can visualize exactly what it would entail"Tease apart the pub/sub and wire-format responsibilities"
In essence what I'd like to do is to replace sse.MessageWriter
with a <-chan *sse.Message
. Then there could be a custom HTTP handler which would look like:
func (s *server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
sess, err := sse.Upgrade(w, r)
if err != nil { /* handle unsupported client */ }
user, err := s.authenticate(r)
if err != nil { /* handle unauthenticated access */ }
msgs := make(chan *sse.Message)
// we can ignore this error because it will probably be just the context error with this update
go joe.Subscribe(r.Context(), sse.Subscription{Chan: msgs, LastEventID: sess.LastEventID, Topics: []string{...}})
for msg := range msgs {
// your own filtering based on permissions
switch err := s.shouldDispatch(r.Context(), user, msg); err.(type) {
case auth.ErrUnauthorized:
continue
default:
/* handle auth errors: e.g. database/auth provider access */
return
}
if err := sess.Send(msg); err != nil {
/* handle write error */
return
}
}
// on ServeHTTP return the request context will be cancelled, https://pkg.go.dev/net/http#Request.Context.
// Joe will respect cancellation and clean up the subscription.
}
sse.Server.OnMessage
callback, similar to the OnSession
one. The implementation would be very easy with the changes from 3, a bit more involved without (it would imply creating a custom MessageWriter
)
http.Handler
nor a new sse.MessageWriter
sse.MessageWriter
though, as you can see above – if I get rid of sse.MessageWriter
there wouldn't even be a need of an sse.Server
, as making one yourself is trivial. The only thing sse.Server
gains the library is easier adoption and better discovery – there's no effort needed to create an http.Handler
and without reading the documentation it's much more probable for a new person to find sse.Server
and start using that than to know to create an sse.Joe
and an http.Handler
. This seems to be a compelling enough argument to ditch minimalism in this case.sse.MessageWriter
, as creating a custom MessageWriter
is complicated enough or implies enough boilerplate for this helper's existence to be justified.http.Handler
themselves. Of course, I'm not required to succumb to every request and I'm in a position to limit sse.Server
's complexity to handle just most use cases, not all – it's just that if for example only 10% of usage is simple enough for sse.Server
to support it then again it may not be worth keeping it in the library.These would be my thoughts. Would be very happy to have some more insight into your issue, as requested in points 1 and 2 above, and your feedback on 3 and 4, and whether any of these solutions would actually help you implement filtering in your application.
Hopefully I've properly understood your problem and gave valuable insight and proposals. Looking forward to your answer!
I'm enjoying this library and impressed by your responsiveness to people's issues!
One problem I'm having is I need to filter events per session based on authorization rules in my security layer. I have a single SSE endpoint that thousands of users connect to, each with unique permissions that influence the events they receive.
I thought about using a custom
MessageWriter
wrapper which might work, but the concreteMessage
struct is highly optimized for streaming and I'd rather not have to unmarshal the message data for every connected client on the way out.I also thought about partitioning my users into separate topics but my permissions are too fine-grained to make this feasible.
Couple of ideas off the top of my head:
Message
an interface so I can attach arbitrary metadatamap[string]any
field toMessage
context.Context
toMessageWriter.Send
so I can grab metadata from thatCheers!