Using ActionCable With a Service Object and Responding to a Message

616 views Asked by At

Making the title for this question was extremely difficult, but essentially, I have a service object in my Rails app that creates a flow for Resume Processing. I am trying to use ActionCable to connect my frontend with my backend. The current way I do this is by instantiating my Service Object in my controller:

  def create_from_resume
    ...
    ResumeParseService.new(@candidate, current_user)
  end

My Service then begins by broadcasting to my front end to open the corresponding modal:

Service:

class ResumeParseService
  attr_reader :user
  attr_reader :employee
  attr_reader :candidate

  def initialize(candidate, user)
    @user = user
    @employee = user.employee
    @candidate = candidate
    @progress = 0

-->    broadcast_begin
  end

  def begin_from_parse_modal
    broadcast_progress(10)

    parsed_resume = get_a_resume_while_hiding_implementation_details
    broadcast_progress(rand(40..60))

    ...

    broadcast_progress(100 - @progress)

    ...
  end

  private


  def broadcast_begin
    ResumeParseChannel.broadcast_and_set_service(self, user, {
      event_name: 'transition_screen',
      props: {
        to: 'parse',
      },
    })
  end

  def broadcast_progress(addition)
    @progress += addition
    ResumeParseChannel.broadcast_to(user, {
      event_name: 'progress',
      props: {
        progress: @progress,
      },
    })
  end

  def broadcast_transition_screen(screen_name, body = nil)
    ResumeParseChannel.broadcast_to(user, {
      event_name: 'transition_screen',
      props: {
        to: screen_name,
        data: body,
      },
    })
  end
end

Rails Channel:

# frozen_string_literal: true
class ResumeParseChannel < ApplicationCable::Channel

  def subscribed
    stream_for(current_user)
  end

  def unsubscribed
    # Any cleanup needed when channel is unsubscribed
  end

  def self.broadcast_and_set_service(service, *args)
    @service = service
    broadcast_to *args
  end

  def screen_transitioned(data)
    case data['screen_name']
    when 'parse'
      pp @service
      @service.begin_from_parse_modal
    else
      # type code here
    end
  end

  private

  def current_user
    if (current_user = env["warden"].user)
      current_user
    else
      reject_unauthorized_connection
    end
  end
end

Which my channel then takes care of. Later, my channel will send back a 'progress update' to let my service know the modal opened successfully: JS Channel:

consumer.subscriptions.create(
  { channel: "ResumeParseChannel" },
  {
    connected() {
      document.addEventListener("resume-parse:screen_transitioned", event =>
-->        this.perform("screen_transitioned", event.detail)
      );
    },
  }
);

Now, my problem is that once that message gets sent back to my (ruby) channel, I can't think of a way for it to find my existing instance of my service object and use it. As you can see, I tried to set an instance var on the channel with the service object instance on the first broadcast, but that (and a million other things) did not work. I need to call #begin_from_parse_modal once I get the 'screen_transitioned' with the screen_name of 'parse'. Ideally, I'd like to separate the broadcasting logic and the parsing logic as much as possible.

I understand that the instance of the channel can be thought of as the actual subscription, but I just don't understand what the best practice is of a system where I can send a "do this" message, and then do something once I get a "its been done" message.

Please let me know if I missed anything in terms of explanation and/or code. Feel free to also let me know if I should do something differently next time I ask something! This is my first time asking on stackoverflow, but it's about my billionth time looking for an answer :)

edit: I'm still dumbfounded by this seemingly common scenario. Could it possibly be best practice to just simply have the channel act as the service object? If so, how would we store state on it? The only possible way I can think of this working in any form is to send the full state in each WS message. Or at least the id's to each record thats in state and then lookup each record on each message. This seems unreasonably complex and expensive. I have scoured other questions and even ActionCable tutorials to find anyone using a service object with receiving messages and have found nothing. SOS!

0

There are 0 answers