# frozen_string_literal: true require "websocket-client-simple" require "securerandom" module WSDirector # WebSocket client class Client include CGI::Escape WAIT_WHEN_EXPECTING_EVENT = 5 attr_reader :ws, :id # Create new WebSocket client and connect to WSDirector # ws URL. # # Optionally provide an ignore pattern (to ignore incoming message, # for example, pings) def initialize(url:, ignore: nil, intercept: nil, subprotocol: nil, headers: nil, id: nil, query: nil, cookies: nil) @ignore = ignore @interceptor = intercept @mailbox = [] has_messages = @has_messages = Concurrent::Semaphore.new(0) messages = @messages = Queue.new open = Concurrent::Promise.new client = self options = {} if subprotocol headers ||= {} headers["Sec-WebSocket-Protocol"] = subprotocol end if cookies headers ||= {} headers["Cookie"] = cookies.map { |_1, _2| "#{_1}=#{escape(_2.to_s)}" }.join("; ") end if query url = "#{url}?#{query.map { |_1, _2| "#{_1}=#{escape(_2.to_s)}" }.join("&")}" end options[:headers] = headers if headers @id = id || SecureRandom.hex(6) @ws = WebSocket::Client::Simple.connect(url, options) do |ws| ws.on(:open) do |_event| open.set(true) end ws.on :message do |msg| data = msg.data next if data.empty? next if client.ignored?(data) next if client.intercepted?(data) messages << data has_messages.release end ws.on :error do |e| messages << Error.new("WebSocket Error #{e.inspect} #{e.backtrace}") end end open.wait!(WAIT_WHEN_EXPECTING_EVENT) rescue Errno::ECONNREFUSED raise Error, "Failed to connect to #{url}" end def each_message @mailbox.dup.each.with_index do |msg, i| yield msg, i end loop do msg = receive @mailbox << msg yield msg, (@mailbox.size - 1) end end def receive(timeout = WAIT_WHEN_EXPECTING_EVENT) @has_messages.try_acquire(1, timeout) msg = @messages.pop(true) raise msg if msg.is_a?(Exception) msg end # Push message back to the mailbox (when it doesn't match the expectation) def consumed(id) @mailbox.delete_at(id) end def send(msg) @ws.send(msg) end def ignored?(msg) return false unless @ignore @ignore.any? { |pattern| msg =~ Regexp.new(pattern) } end def intercepted?(msg) return false unless @interceptor instance_exec(msg, &@interceptor) end end end