-
Notifications
You must be signed in to change notification settings - Fork 369
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
Provide entire message including channel to subscription callbacks #385
Conversation
…extensions, as a second argument to subscription callbacks. Does not break API
Tossing in my "+1". Patch is succinct. Passing additional information around in the message payload as "duplicate" information does not seem as elegant as this modification appears to be. |
This change makes me nervous, for several reasons. It conflates several concerns that the Faye client deliberately keeps separate. Those concerns are:
The client currently only exposes the first of these to the user, by design. The extension layer exposes the second. This means that users, on first using the library, are not presented with extraneous data; the data the comes out of The separation between 1 and 2 also tends to keep certain kinds of operations apart. Extensions are frequently used to mutate data between the client and the transport layer. Every message goes through your extensions once and only once, and are passed to each extension sequentially rather than in parallel. (Messages are not passed through extensions a second time if the transport needs to retry sending them.) This means you can mutate it and be sure that nothing else is using the message at the time you're mutating it. Functions passed to This also points to a potential usability pitfall in this design: it will not be immediately apparent that the Having an event listener emit more than one value makes the use of higher-level abstractions harder, because it makes listeners harder to compose. For example, This points to a subtlety in what we mean by 'breaking change'. Yes, in JavaScript you can add a parameter to a function and existing callers will not throw errors. However this is an accident of JavaScript's design; a client in Ruby that uses lambdas or bound methods for callbacks would break if the arity of Even if you're not using a formal abstraction like a stream or some other functor, this is likely to require downstream changes compared to the solution I proposed in #119: listeners will often hand a message off to other functions for processing, and if the listener receives more arguments, those must also be handed off to processing functions, and such functions and their call sites will need to be changed to do this. (This is really just an informal way of saying that this change breaks the type of the event stream.) Finally, this change encourages users to conflate 1 and 3, which I think is an anti-pattern. To me, a channel is just a "thing you get data out of", it is not itself meaningful data. If you think of every channel as a stream, then subscribing to a wildcard is equivalent to constructing the union of all the streams matching that pattern. Picture what happens if you do the equivalent thing with arrays: var arr1 = ['alice', 'bob', 'carol'],
arr2 = ['dan', 'eve', 'fred'];
var merged = arr1.concat(arr2); If you then try to process
This allows the response to be processed without having to carry around the URL it was derived from. I would apply the same pattern when using Faye: if a message is completely self-contained, it can be passed around to any part of the application, and those sites won't need to know which channel the message came from, or even that it came from Faye at all. This allows for decoupling and stops the application become deeply coupled to the Faye client or the Bayeux protocol. However, if you do need to identify the source of the message, the small extension I propose in #119 lets you do this while retaining all the benefits I have outlined above, because from the |
I should clarify that the above is just me pedantically working through my own reasoning about why I might feel a certain way about a change. Although I have tried to focus on objective properties of the code, all of it is in service of weighing something very fuzzy (i.e. usability) and I might be wrong. That's why I've not closed this issue -- if I'm wrong, I'd like to be told. |
Hi @jcoglan While I do understand the motivation between this choice to keep everything isolated, I still think this missing features breaks expectations for developers as most messaging solutions does this. It pushes people to add the information to the data hash (what we do currently), but this feels a bit hackish IMO. Also I think adding the channel as a second parameter is the good option and would not hurt compatibility, even in ruby as you can check the arity of the callback to ensure full compatility: def on_message(&block)
block.arity == 2 ? yield('data', 'channel') : yield('data')
end — http://rubyfiddle.com/riddles/e689b Finally, I like your simple extension suggestion in #119, but it's a bit annoying to have to implement it yourself and add it everywhere you publish (it can be from several places or processes, js & ruby) We can update our PR (#395, which is most complete about this feature I think) with arity check to be fully backward-compatible if you're interested. |
I am in support of PR #395. Any pubsub interface I've used (admittedly not many) that supported wildcard subscriptions would pass the channel as an argument to the message callback. I understand @jcoglan's reasoning and would suggest that PR #395 be modified to toggle this behavior based on a configuration option passed to the client constructor. Basically, it should be opt-in for those who want it. That way it won't break backwards compatibility and those who do opt-in will be aware of the potential breakage or special handling needed for Streams or Promises or whatever. That's my take. :) |
Sorry for taking my time over this. While I still think there are better solutions to this problem, as I outlined above, I understand this is a convenience that a bunch of people expect (seeing as there have been multiple similar PRs about it). I would like to introduce something that we can be sure won't break existing code. I don't want to introduce new parameters into existing callbacks, since that would break variadic functions and prevent conversion of such callbacks into a streams as I mentioned above. Arity checking is a hack as is really complicated to get right in general, and is useless if the actual subscriber function is being indirected through another function anyway. I think the cleanest way to add this feature is to introduce a new method that explicitly signals that you want the channel as an argument. I am not going to yield the entire message because that would mix application and protocol data. It breaks the symmetry of "what you pass to (We also have extensions entirely so people can make changes like this one without needing the core client to be changed, but I understand that over time some common use cases can make their way into core. But I also think that requiring subscribers to know where a message came from, because the message's own content is insufficient to process it, produces highly coupled designs, and so it should feel like a hack if you want that behaviour, but I digress.) Here's my proposal. 52d6df2 adds a new method to // if I publish {"channel": "/chat/alice", "data": {"hello": "world"}}, then
// channel is '/chat/alice' and message is {hello: 'world'}
client.subscribe('/chat/*').withChannel(function(channel, message) { /* ... */ });
var sub = client.subscribe('/chat/*').withChannel(function(channel, message) { /* ... */ });
sub.then(function() { /* ... */ }); I've put How do people feel about this design? If it seems okay then I will add more tests and port it to Ruby, and release it in version 1.2. |
Seems like a good option to me, it reads nice and doesn't break anything. |
Does there happen to be a client level event emitted for messages? Something like this would be good enough for my needs: client.on('message', function (channel, message) { /* ... */ });
client.subscribe('/chat/*'); Otherwise, |
Since I chimed in before, I'll go ahead and add my $0.01; I like the 'withChannel' idea. Your reasoning seems spot-on. Since I have already written workarounds long ago I don't know when I will have the chance to test this new mechanism but I might try porting/converting an existing implementation just to see how it goes. Timeframe TBD, unfortunately. |
Is |
The |
Thanks for all your hard work @jcoglan |
Provide the entire message object, including channel, id, and any data added by extensions, as a second argument to subscription callbacks.
This does not break the API in any way, and mirrors the data provided to extensions.
This fixes issue #119