Closed leastbad closed 4 years ago
I like the idea of supporting something like this in CableReady:
cable_ready[stream_name].refresh
That is smart enough to perform any of the following based on detected support.
Wow, that would be cool.
Circling back on this, it seems like the pattern that works best is to use CableReady to broadcast a message to the client. This channel handler then initiates a SR stimulate() operation, refreshing the content. I've found that a good strategy is to create a Stimulus controller that is also an ActionCable channel consumer. Indeed, I don't actually have a channels folder as all of my channels are managed by Stimulus controllers, which can easily call StimulusReflex.
By way of example, here is a complete polymorphic comment system implemented via SR and CableReady:
class CommentChannel < ApplicationCable::Channel
def subscribed
stream_for params[:commentable_type].constantize.find(params[:commentable_id])
end
end
import { Controller } from 'stimulus'
import StimulusReflex from 'stimulus_reflex'
import consumer from '../lib/consumer'
export default class extends Controller {
static targets = ['comment']
connect () {
this.element[this.identifier] = this
StimulusReflex.register(this)
if (this.element.dataset.commentableId) {
const controller = this
consumer.subscriptions.create(
{
channel: 'CommentChannel',
commentable_type: this.element.dataset.commentableType,
commentable_id: this.element.dataset.commentableId
},
{
received () {
controller.stimulate('ApplicationReflex#refresh')
}
}
)
}
}
comment (e) {
e.preventDefault()
if (this.commentTarget.value.length)
this.stimulate('CommentsReflex#comment', {
class: this.element.dataset.commentableType,
id: this.element.dataset.commentableId,
body: this.commentTarget.value
})
this.commentTarget.value = ''
}
}
class Comment < ApplicationRecord
belongs_to :commentable, polymorphic: true, optional: true
belongs_to :user
after_save do
CommentChannel.broadcast_to(commentable, {})
end
end
class CommentsReflex < StimulusReflex::Reflex
delegate :current_user, to: :connection
def comment(payload)
current_user.comments.create({
body: payload["body"],
commentable_type: payload["class"],
commentable_id: payload["id"],
})
end
end
The after_save callback in the Events model could just as easily be an ActiveJob perform
method.
Also, the ApplicationReflex#refresh is an empty method that simply forces a content refresh.
I want to demonstrate triggering the delivery of a Reflex payload from outside of the request loop. That could be in a few milliseconds or an arbitrary amount of time later. The key detail is that it's generated in response to an external event such as a webhook or notification.
I figured that a good teaching vehicle would be an ActionJob. I can send in the same parameters that are used to call channel.receive() + the stream_name.
Except, wait... you hit logical flaw number one: when you create an instance of StimulusReflex::Channel, you need to pass it a Connection so that the render_page method has a valid ActionDispatch::Request object.
Huh! Okay, what are my other options? Well, I could make use of the new
ApplicationController.renderer.render(partial: 'messages/message', locals: { message: message })
thing and then callActionCable.server.broadcast
directly.The problem there is that I'd have to call both the reflex action AND the controller action in order to properly replicate the instance variables for the template. eff that
Which brings me full circle to questioning my original premise for wait_for_it. However, maybe the fact that it could serve literally the wrong page (if the user has navigated away) is a sign that this is Not The Right Way To Do This.
Where I landed was a different idea altogether. We're already talking about adding the capacity to do a Turbolinks/classic browser redirect operation. What if we used [the same or a similar mechanism] to send a message to the client that says: hey, if you're still on URL X, there is a potentially newer version of the page which you could refresh.
The advantages of this approach are that it gives implementation flexibility to the developer in terms of handling how/when/if they want to handle out-of-band updates, but also that a request coming from the client over the existing connection is going to have a valid connection + stream_name and all of the other important details otherwise absent.
The only other idea I had was that perhaps there is a unique ID attached to each connection object, and that this ID could be passed into the ActionJob and later used to obtain a reference to the connection for purposes of instantiating a new StimulusReflex::Channel.
Alright, I'm spent. Who has opinions?! :smiling_imp: