Streaming from background job and race conditions

I have a link that opens a new tab, this controller’s action would schedule a job and render a page subscribing to "pdf_rendering:#{doc_source.id}" channel (doc source is a model, ID is UUID, not super relevant, but maybe this context is helpful)

When the job succeeds, it sends Turbo::StreamsChannel.broadcast_replace_to("pdf_rendering:#{doc_source.id}", ...) rendering a partial that would replace my please wait... frame with the proper contents.

This works perfectly if and only if the Job finishes after the page rendered and successfully subscribed to the channel. If the job was quick - the page renders and subscribes after the stream notification was emitted and nothing would happen.

I was thinking about a solution where I use the src attribute on my frame, so once it loads it would send a turbo-stream request again, but it seems to be not guaranteed that the channel subscription will happen before the frame sends a request

It also seems there’s no event like turbo-stream-sunscribed I could hook up to, and perform Turbo.visit (instead of relying on the load using src attribute).

What are my options? Is this an edge case not yet dealt with?
I found some topics like:

Or is there already an idiomatic way to deal with that?

Currently, I delay my job by a few seconds, I might show a “Click here to check” button to appear after some time, but those feel like half measures.
Considering long-polling as a last option, seems weird to implement such a thing when I have a working web socket connection.

Another possibility would be to record the time from before I scheduled the job, and then, in my view, subscribe to the channel and somehow get past events after the timestamp - but this feels like going against the current ActionCable architecture and philosophy.

I’m curious on what your thoughts are.

Follow up:

I was digging a bit, and there appears to be a way to check if I’m subscribed to the channel, and it goes a bit like this

// temp0 is a reference to <turbo-cable-stream-source> DOM node
temp0.subscription.consumer.subscriptions.subscriptions.map(
  (i) => { 
    return atob(
      JSON.parse(i.identifier).signed_stream_name.split("--")[0]) 
  }
)

This seems to produce a list of streams , and (I assume) one being there means the page subscribed. I checked by disabling my /cable endpoint, the stream is still there, so the identifier does not indicate the connection being established.

A bit plenty of assumptions must be made along the way, but the information is there :sweat_smile:

Update: for now, I settled for such solution:

  1. #show action renders a page which
  • subscribes to "pdf_rendering:#{doc_source.id}"
  • renders a form (to #create action) and using a stimulus controller submits it on load@window event
  1. #create action is responsible for scheduling the background job (which in turn will broadcast the turbo replace)

This seems more in line with RESTful approach, and it doesn’t feel like I’m fighting with the framework.

Still interested in your thoughts, though.

Are you using Turbo? What if you mark the element with the subscription as data-turbo-permanent and manage its state with stimulus? After the form submits, the element/subscription would persist. I haven’t tried this with a turbo stream from, but was my first thought. Alternatively, you could manage it yourself through actioncable, but feels like there should be a restful way to achieve this.

It is possible to do logic in your Channel class so when somebody subscribes, you broadcast an event immediately. So in your example if your doc_source model “knows” that the background job has been completed you might try something like this in your Channel code:

class DocSourceChannel < ApplicationCable::Channel
  def subscribed
    stream_from "doc_source:#{params[:id]}"
    doc_source = DocSource.find(id: params[:id])

    if doc_source&.pdf_generated?
      self.class.broadcast_to doc_source.id, status: 'completed'
    end

  end
...

I’m just making guesses about your model names etc, but hopefully you get the idea