# frozen_string_literal: true class StimulusReflex::Channel < StimulusReflex.configuration.parent_channel.constantize def stream_name ids = connection.identifiers.map { |identifier| send(identifier).try(:id) || send(identifier) } [ params[:channel], ids.select(&:present?).join(";") ].select(&:present?).join(":") end def subscribed super fix_environment! stream_from stream_name end def receive(data) url = data["url"].to_s selectors = (data["selectors"] || []).select(&:present?) selectors = data["selectors"] = ["body"] if selectors.blank? target = data["target"].to_s reflex_name, method_name = target.split("#") reflex_name = reflex_name.camelize reflex_name = reflex_name.end_with?("Reflex") ? reflex_name : "#{reflex_name}Reflex" arguments = (data["args"] || []).map { |arg| object_with_indifferent_access arg } element = StimulusReflex::Element.new(data) permanent_attribute_name = data["permanentAttributeName"] form_data = Rack::Utils.parse_nested_query(data["formData"]) params = form_data.deep_merge(data["params"] || {}) begin begin reflex_class = reflex_name.constantize.tap { |klass| raise ArgumentError.new("#{reflex_name} is not a StimulusReflex::Reflex") unless is_reflex?(klass) } reflex = reflex_class.new(self, url: url, element: element, selectors: selectors, method_name: method_name, params: params, client_attributes: { reflex_id: data["reflexId"], xpath: data["xpath"], c_xpath: data["cXpath"], reflex_controller: data["reflexController"], permanent_attribute_name: permanent_attribute_name }) delegate_call_to_reflex reflex, method_name, arguments rescue => invoke_error message = exception_message_with_backtrace(invoke_error) body = "Reflex #{target} failed: #{message} [#{url}]" if reflex reflex.rescue_with_handler(invoke_error) reflex.broadcast_message subject: "error", body: body, data: data, error: invoke_error else puts "\e[31m#{body}\e[0m" if body.to_s.include? "No route matches" initializer_path = Rails.root.join("config", "initializers", "stimulus_reflex.rb") puts <<~NOTE \e[33mNOTE: StimulusReflex failed to locate a matching route and could not re-render the page. If your app uses Rack middleware to rewrite part of the request path, you must enable those middleware modules in StimulusReflex. The StimulusReflex initializer should be located at #{initializer_path}, or you can generate it with: $ bundle exec rails generate stimulus_reflex:config Configure any required middleware: StimulusReflex.configure do |config| config.middleware.use FirstRackMiddleware config.middleware.use SecondRackMiddleware end\e[0m NOTE end end return end if reflex.halted? reflex.broadcast_message subject: "halted", data: data else begin reflex.broadcast(selectors, data) rescue => render_error reflex.rescue_with_handler(render_error) message = exception_message_with_backtrace(render_error) body = "Reflex failed to re-render: #{message} [#{url}]" reflex.broadcast_message subject: "error", body: body, data: data, error: render_error puts "\e[31m#{body}\e[0m" end end ensure if reflex commit_session(reflex) reflex.logger.print end end end private def object_with_indifferent_access(object) return object.with_indifferent_access if object.respond_to?(:with_indifferent_access) object.map! { |obj| object_with_indifferent_access obj } if object.is_a?(Array) object end def is_reflex?(reflex_class) reflex_class.ancestors.include? StimulusReflex::Reflex end def delegate_call_to_reflex(reflex, method_name, arguments = []) method = reflex.method(method_name) required_params = method.parameters.select { |(kind, _)| kind == :req } optional_params = method.parameters.select { |(kind, _)| kind == :opt } if arguments.size == 0 && required_params.size == 0 reflex.process(method_name) elsif arguments.size >= required_params.size && arguments.size <= required_params.size + optional_params.size reflex.process(method_name, *arguments) else raise ArgumentError.new("wrong number of arguments (given #{arguments.inspect}, expected #{required_params.inspect}, optional #{optional_params.inspect})") end end def commit_session(reflex) store = reflex.request.session.instance_variable_get("@by") store.commit_session reflex.request, reflex.controller.response rescue => e message = "Failed to commit session! #{exception_message_with_backtrace(e)}" puts "\e[31m#{message}\e[0m" end def exception_message_with_backtrace(exception) "#{exception}\n#{exception.backtrace.first}" end def fix_environment! ([ApplicationController] + ApplicationController.descendants).each do |controller| controller.renderer.instance_variable_set(:@env, connection.env.merge(controller.renderer.instance_variable_get(:@env))) end end end