-
Notifications
You must be signed in to change notification settings - Fork 337
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
[WIP] Add support for Ring websocket API #546
Conversation
I think the failure in the main test is unrelated to my changes. It looks like the |
Hi James, thanks for this! Will try take a closer look tomorrow, but in the meantime wanted to respond to this-
Correct, the failure's very possibly unrelated. Looks like the timing for that test may not leave much margin for error if CI is slow, I'll bump the allowed time. |
👍 Thanks for the clear status, and all your work on this James! Took a quick skim at the current PR, looks broadly reasonable from my end. Only concern that stuck out - it looks like your Is access the One of http-kit's major value propositions is it's lack of dependencies. Edit: forgot to add, I've bumped the failing test's allotted time - so hope that issue might resolve if you rebase your PR. |
I wouldn't be too happy about the hard-coded dependency on ring since this has an impact on graalvm binaries like babashka, their size would become bigger (and there is more code to worry about getting right in relation to graalvm native-image). If possible I'd appreciate if this could be done simply by passing a function to |
This is exactly the sort of feedback I was hoping to get, as it's something I wouldn't have considered otherwise. So first of all, thank very much for taking the time to review this. What if I split the relevant protocols out into their own library, so I'd have a (ns ring.websocket.protocols)
(defprotocol Listener
(on-open [listener socket])
(on-message [listener socket message])
(on-pong [listener socket data])
(on-error [listener socket throwable])
(on-close [listener socket code reason]))
(defprotocol PingListener
(on-ping [listener socket data]))
(defprotocol Socket
(-open? [socket])
(-send [socket message])
(-ping [socket data])
(-pong [socket data])
(-close [socket code reason]))
(defprotocol AsyncSocket
(-send-async [socket message succeed fail])) While this would still add a dependency, it would be as minimal as possible with no code beside the relevant protocols. Would this be acceptable? |
Seems like an idea. I wonder if there's ways to do it without adding dependencies at all. "Just use maps"? Or maybe that would be too brittle? |
I guess another idea would be to add: 1 a protocol to http-kit Maybe that's too much overkill though |
A note: ring-core pulls in more transitive dependencies as well:
Maybe another good argument to split things up. My question would be: would the existing websocket functionality (as of before this PR) keep on working, when you do not include the full |
We could do something like this, where both the listener and socket are maps of functions: {:ring.websocket/listener
{:on-open (fn [{:keys [send ping open?]}]
(when (open?)
(ping)
(send "Hello World"))}} The problem is that this would be less performant than using protocols, the functions wouldn't be enforced by the compiler, and it feels less idiomatic overall. I think my preference would still lean toward having a minimal protocol library.
Currently no, because |
Just to be clear, websocket functionality will keep on working as it is right now (on master), without adding a dependency on ring-core, but just a small dependency on a protocol-only dependency? Sounds good enough to me! |
With "now" I mean as things are on master, not as they are in this PR. |
Yes, exactly. |
That'd definitely be preferable, but I do think we should try rule out dependency-free alternatives first. @weavejester James, http-kit already contains a fair bit of conditional code (e.g. here). Perhaps instead of unconditionally pulling in the relevant Ring dependency, we could instead check if it's already loaded/present and act accordingly? |
If we want a purely dynamic load, then for the socket, we could use a metadata protocol: (defn- channel->ring-socket [^AsyncChannel ch]
(with-meta
{:channel ch}
{'ring.websocket.protocols/open? (fn [_] (not (.isClosed ch)))
...})) I haven't tried it before, but I believe this should work. And for the listener, we could use a var lookup: (defn- channel->listener [^AsyncChannel ch listener]
(require 'ring.websocket.protocols)
(let [on-message (var-get (find-var 'ring.websocket.protocols/on-message))]
(.setReceiveHandler ch #(on-message listener ch (->ring-message %)))
...)) There would probably be some performance loss, but I'm unsure if that would matter much for something like websockets that are mostly going to be limited by I/O. However, regardless of whether we use that, I think it's still a good idea to separate out the Ring websocket protocols into their own library. |
Better to avoid "inline" |
So something like: (defn- wrap-ring-websocket [handler]
(utils/compile-if (require 'ring.websocket.protocols)
;; ring websocket code
handler)) Would that work in GraalVM? I guess we could also use |
Yeah, that would work, I think.
|
Ah, sorry, my example probably wasn't clear. The |
Gotcha! |
Small details, the condition in
would be necessary as the condition. |
5d574b4
to
9e8c54e
Compare
Should not yet be merged. For review purposes only.
Changes Ring websocket dependency to a small protocol-only library.
Jetty 11 has SNI checks enabled by default, and for testing with localhost they need to be turned off.
9e8c54e
to
f2410f6
Compare
I've updated the code so that Ring WebSockets are an optional dependency, and that dependency is a small library consisting of only protocols. I think the tests need a little more work to get them passing more reliably, and the Ring dependencies will need updating once we hit an RC, but overall I think it's done for now. |
I'd be interested in one test suite which checks if the existing websocket stuff still runs without the ring dependency as well. |
That's a good point. Maybe we just add a Leiningen profile that omits the dependency, and add that to the GitHub Actions tests. |
@weavejester Hi James, happy new year! Just confirming that this is ready to go from your side? I'm happy to update the deps and add the remaining deps-free tests when merging. |
Yes it should be ready aside from the updated deps and the deps-free tests that you mentioned. I also separated out the websocket protocols into a minimal |
…avejester) Jetty 11 has SNI checks enabled by default, and for testing with localhost they need to be turned off.
This is: - Only available when `ring.websocket.protocols` is available (usually via a `ring-core` dependency). - Fully opt-in. It won't affect any pre-existing http-kit code by default, and can be ignored by folks that'd rather stick to http-kit's traditional WebSocket API.
Merged manually 👍 On master now, will be included in the next release ( Thanks again for all the work on this! 🙏 |
This PR should not yet be merged, as the Ring websocket API is not stable. However, I wanted to implement the alpha websocket API for different adapters in order to head off any difficulties or flaws in the API design.
Implementing this for http-kit proved to be more difficult than I expected, as http-kit abstracts things like the close handler, and the Ring API is a little more low level. As an overview:
pongHandler
to handle pong framescloseRingHandler
to handle close frames and pass the raw code and reason datasend
I use some private middleware to detect Ring websocket responses and convert them into a http-kit channel.
This PR will eventually close #541.