bidi2pdf 0.1.5 → 0.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +4 -1
  3. data/CHANGELOG.md +61 -1
  4. data/README.md +47 -10
  5. data/docker/Dockerfile +11 -3
  6. data/docker/Dockerfile.chromedriver +4 -2
  7. data/docker/Dockerfile.slim +75 -0
  8. data/lib/bidi2pdf/bidi/browser_console_logger.rb +92 -0
  9. data/lib/bidi2pdf/bidi/browser_tab.rb +415 -39
  10. data/lib/bidi2pdf/bidi/client.rb +85 -23
  11. data/lib/bidi2pdf/bidi/command_manager.rb +46 -48
  12. data/lib/bidi2pdf/bidi/commands/base.rb +39 -1
  13. data/lib/bidi2pdf/bidi/commands/browser_remove_user_context.rb +27 -0
  14. data/lib/bidi2pdf/bidi/commands/browsing_context_print.rb +4 -0
  15. data/lib/bidi2pdf/bidi/commands/print_parameters_validator.rb +5 -0
  16. data/lib/bidi2pdf/bidi/commands.rb +1 -0
  17. data/lib/bidi2pdf/bidi/event_manager.rb +1 -1
  18. data/lib/bidi2pdf/bidi/interceptor.rb +1 -1
  19. data/lib/bidi2pdf/bidi/js_logger_helper.rb +16 -0
  20. data/lib/bidi2pdf/bidi/logger_events.rb +66 -0
  21. data/lib/bidi2pdf/bidi/network_event.rb +40 -7
  22. data/lib/bidi2pdf/bidi/network_event_formatters/network_event_console_formatter.rb +110 -0
  23. data/lib/bidi2pdf/bidi/network_event_formatters/network_event_formatter_utils.rb +53 -0
  24. data/lib/bidi2pdf/bidi/network_event_formatters/network_event_html_formatter.rb +125 -0
  25. data/lib/bidi2pdf/bidi/network_event_formatters.rb +11 -0
  26. data/lib/bidi2pdf/bidi/network_events.rb +46 -17
  27. data/lib/bidi2pdf/bidi/session.rb +120 -13
  28. data/lib/bidi2pdf/bidi/user_context.rb +62 -0
  29. data/lib/bidi2pdf/bidi/web_socket_dispatcher.rb +7 -7
  30. data/lib/bidi2pdf/chromedriver_manager.rb +48 -21
  31. data/lib/bidi2pdf/cli.rb +27 -3
  32. data/lib/bidi2pdf/dsl.rb +33 -0
  33. data/lib/bidi2pdf/launcher.rb +34 -2
  34. data/lib/bidi2pdf/notifications/event.rb +52 -0
  35. data/lib/bidi2pdf/notifications/instrumenter.rb +65 -0
  36. data/lib/bidi2pdf/notifications/logging_subscriber.rb +136 -0
  37. data/lib/bidi2pdf/notifications.rb +78 -0
  38. data/lib/bidi2pdf/session_runner.rb +49 -7
  39. data/lib/bidi2pdf/verbose_logger.rb +79 -0
  40. data/lib/bidi2pdf/version.rb +1 -1
  41. data/lib/bidi2pdf.rb +99 -5
  42. data/sig/bidi2pdf/bidi/client.rbs +1 -1
  43. metadata +45 -4
  44. data/lib/bidi2pdf/utils.rb +0 -15
@@ -0,0 +1,110 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bidi2pdf
4
+ module Bidi
5
+ module NetworkEventFormatters
6
+ # rubocop: disable Metrics/AbcSize
7
+ class NetworkEventConsoleFormatter
8
+ include NetworkEventFormatterUtils
9
+
10
+ attr_reader :color_enabled, :logger
11
+
12
+ # ANSI styles
13
+ RESET = "\e[0m"
14
+ BOLD = "\e[1m"
15
+ DIM = "\e[2m"
16
+ RED = "\e[31m"
17
+ GREEN = "\e[32m"
18
+ YELLOW = "\e[33m"
19
+ CYAN = "\e[36m"
20
+ GRAY = "\e[90m"
21
+
22
+ def initialize(color: true, logger: Bidi2pdf.network_events_logger)
23
+ @color_enabled = color
24
+ @logger = logger
25
+ end
26
+
27
+ def log(events)
28
+ events.each { |event| pretty_log(event).each_line { |line| logger.info(line.chomp) } }
29
+ end
30
+
31
+ def pretty_log(event)
32
+ status = event.http_status_code ? "HTTP #{event.http_status_code}" : "pending"
33
+ status_color = color_for_status(event.http_status_code)
34
+ start = event.format_timestamp(event.start_timestamp)
35
+ finish = event.end_timestamp ? event.format_timestamp(event.end_timestamp) : dim("...")
36
+ duration = event.duration_seconds ? cyan("#{event.duration_seconds}s") : dim("in progress")
37
+ timing_details = format_timing(event)
38
+ bytes = event.bytes_received ? format_bytes(event.bytes_received) : dim("N/A")
39
+ displayed_url = shorten_url(event.url)
40
+
41
+ <<~LOG.strip
42
+ #{bold("┌─ Network Event ──────────────────────────────────────")}
43
+ #{bold("│ Request: ")}#{event.http_method || "?"} #{displayed_url}#{" "}
44
+ #{bold("│ State: ")}#{event.state}
45
+ #{bold("│ Status: ")}#{status_color}#{status}#{reset}
46
+ #{bold("│ Started: ")}#{start}
47
+ #{bold("│ Ended: ")}#{finish}
48
+ #{bold("│ Duration:")} #{duration}
49
+ #{bold("│ Received:")} #{bytes}
50
+ #{timing_details}
51
+ #{bold("└──────────────────────────────────────────────────────")}
52
+ LOG
53
+ end
54
+
55
+ private
56
+
57
+ def format_timing(event)
58
+ return "" unless event.timing.is_a?(Hash)
59
+
60
+ keys = %w[
61
+ requestTime proxyStart proxyEnd dnsStart dnsEnd connectStart connectEnd
62
+ sslStart sslEnd workerStart workerReady sendStart sendEnd receiveHeadersEnd
63
+ ]
64
+
65
+ visible = keys.map do |key|
66
+ next unless event.timing[key]
67
+
68
+ label = key.gsub(/([A-Z])/, ' \1').capitalize
69
+ "#{dim("│")} #{label.ljust(20)}: #{event.timing[key].round(2)} ms#{reset}"
70
+ end.compact
71
+
72
+ return "" if visible.empty?
73
+
74
+ [dim("│").to_s, dim("│ Timing Phases:").to_s].concat(visible).join("\n")
75
+ end
76
+
77
+ # === Color Helpers ===
78
+
79
+ def color_for_status(code)
80
+ return gray unless code
81
+
82
+ case code.to_i
83
+ when 200..299 then green
84
+ when 300..499 then yellow
85
+ when 500..599 then red
86
+ else gray
87
+ end
88
+ end
89
+
90
+ def bold(str) = color_enabled ? "#{BOLD}#{str}#{RESET}" : str
91
+
92
+ def dim(str) = color_enabled ? "#{DIM}#{str}#{RESET}" : str
93
+
94
+ def green(str = "") = color_enabled ? "#{GREEN}#{str}" : str
95
+
96
+ def yellow(str = "") = color_enabled ? "#{YELLOW}#{str}" : str
97
+
98
+ def red(str = "") = color_enabled ? "#{RED}#{str}" : str
99
+
100
+ def cyan(str = "") = color_enabled ? "#{CYAN}#{str}" : str
101
+
102
+ def gray(str = "") = color_enabled ? "#{GRAY}#{str}" : str
103
+
104
+ def reset = color_enabled ? RESET : ""
105
+
106
+ # rubocop: enable Metrics/AbcSize
107
+ end
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "cgi"
4
+
5
+ module Bidi2pdf
6
+ module Bidi
7
+ module NetworkEventFormatters
8
+ module NetworkEventFormatterUtils
9
+ def format_bytes(size)
10
+ return "N/A" unless size.is_a?(Numeric)
11
+
12
+ units = %w[B KB MB GB TB]
13
+ idx = 0
14
+ while size >= 1024 && idx < units.size - 1
15
+ size /= 1024.0
16
+ idx += 1
17
+ end
18
+ format("%<size>.2f %<unit>s", size: size, unit: units[idx])
19
+ end
20
+
21
+ def parse_timing(event)
22
+ return [] unless event.timing.is_a?(Hash)
23
+
24
+ keys = %w[
25
+ requestTime proxyStart proxyEnd dnsStart dnsEnd connectStart connectEnd
26
+ sslStart sslEnd workerStart workerReady sendStart sendEnd receiveHeadersEnd
27
+ ]
28
+
29
+ keys.filter_map do |key|
30
+ next unless event.timing[key]
31
+
32
+ label = key.gsub(/([A-Z])/, ' \1').capitalize
33
+ { label: label, key: key, ms: event.timing[key].round(2) }
34
+ end
35
+ end
36
+
37
+ def format_timestamp(timestamp)
38
+ return "N/A" unless timestamp
39
+
40
+ Time.at(timestamp.to_f / 1000).utc.strftime("%Y-%m-%d %H:%M:%S.%L UTC")
41
+ end
42
+
43
+ def shorten_url(url)
44
+ sanitized_url = CGI.escapeHTML(url)
45
+
46
+ return sanitized_url unless sanitized_url.start_with?("data:text/html")
47
+
48
+ "data:text/html,..."
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,125 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bidi2pdf
4
+ module Bidi
5
+ module NetworkEventFormatters
6
+ class NetworkEventHtmlFormatter
7
+ include NetworkEventFormatterUtils
8
+
9
+ def render(events)
10
+ return unless Bidi2pdf.network_events_logger.info?
11
+
12
+ <<~HTML
13
+ <!DOCTYPE html>
14
+ <html lang="en" data-bs-theme="light">
15
+ <head>
16
+ <meta charset="UTF-8">
17
+ <title>Network Events</title>
18
+ <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
19
+ <style>
20
+ body { font-family: monospace; padding: 2rem; }
21
+ .event { background: var(--bs-body-bg); border: 1px solid var(--bs-border-color); padding: 1rem; margin-bottom: 2rem; border-radius: .5rem; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
22
+ .bar-container { position: relative; height: 20px; background: var(--bs-secondary-bg); border-radius: 4px; }
23
+ .bar { position: absolute; top: 0; height: 100%; background-color: #0d6efd; opacity: 0.8; }
24
+ .toc a { text-decoration: none; display: block; margin-bottom: 0.5rem; }
25
+
26
+ @media print {
27
+ .no-break {
28
+ page-break-inside: avoid;
29
+ }
30
+ #{" "}
31
+ #{" "}
32
+ .form-select, .form-label, #theme-select {
33
+ display: none !important; /* Hide theme selector when printing */
34
+ }
35
+ }
36
+
37
+ .event {
38
+ word-break: break-word;
39
+ overflow-wrap: anywhere;
40
+ }
41
+ </style>
42
+ <script>
43
+ function toggleTheme(value) {
44
+ document.documentElement.setAttribute('data-bs-theme', value);
45
+ }
46
+ </script>
47
+ </head>
48
+ <body>
49
+ <h1>Network Events</h1>
50
+ <div class="mb-4">
51
+ <label for="theme-select" class="form-label">Theme:</label>
52
+ <select id="theme-select" class="form-select w-auto d-inline-block" onchange="toggleTheme(this.value)">
53
+ <option value="light">Light</option>
54
+ <option value="dark">Dark</option>
55
+ </select>
56
+ </div>
57
+
58
+ <h2>Index</h2>
59
+ <div class="toc mb-4">
60
+ #{events.map.with_index { |e, i| toc_entry(e, i) }.join("\n")}
61
+ </div>
62
+
63
+ #{events.map.with_index { |e, i| render_event(e, i) }.join("\n")}
64
+ </body>
65
+ </html>
66
+ HTML
67
+ end
68
+
69
+ def toc_entry(event, index)
70
+ "<a href=\"#event-#{index}\">[#{index + 1}] #{event.http_method} #{event.url}</a>"
71
+ end
72
+
73
+ # rubocop: disable Metrics/AbcSize
74
+ def render_event(event, index)
75
+ timing = parse_timing(event)
76
+ duration = event.duration_seconds || 0
77
+ duration_str = event.in_progress? ? "in progress" : "#{duration}s"
78
+ status = event.http_status_code || "?"
79
+ method = event.http_method || "?"
80
+ start = format_timestamp(event.start_timestamp)
81
+ finish = event.end_timestamp ? format_timestamp(event.end_timestamp) : "..."
82
+ bytes = event.bytes_received ? format_bytes(event.bytes_received) : "N/A"
83
+ bars = render_timing_bars(timing)
84
+ displayed_url = shorten_url(event.url)
85
+
86
+ <<~HTML
87
+ <div class="event no-break" id="event-#{index}">
88
+ <div><strong>Request:</strong> #{method} #{displayed_url}</div>
89
+ <div><strong>Status:</strong> HTTP #{status}</div>
90
+ <div><strong>State:</strong> #{event.state}</div>
91
+ <div><strong>Start:</strong> #{start}</div>
92
+ <div><strong>End:</strong> #{finish}</div>
93
+ <div><strong>Duration:</strong> #{duration_str}</div>
94
+ <div><strong>Received:</strong> #{bytes}</div>
95
+ #{bars}
96
+ </div>
97
+ HTML
98
+ end
99
+
100
+ # rubocop: enable Metrics/AbcSize
101
+
102
+ def render_timing_bars(timing)
103
+ return "" if timing.empty?
104
+
105
+ max_ms = timing.map { |t| t[:ms] }.max
106
+ scale = max_ms.zero? ? 0 : 100.0 / max_ms
107
+
108
+ bars = timing.map do |t|
109
+ width = (t[:ms] * scale).clamp(1, 100).round(2)
110
+ <<~HTML
111
+ <div>
112
+ <small>#{t[:label]} (#{t[:ms]} ms)</small>
113
+ <div class="bar-container mb-2">
114
+ <div class="bar" style="width: #{width}%"></div>
115
+ </div>
116
+ </div>
117
+ HTML
118
+ end
119
+
120
+ "<div class=\"mt-3\"><strong>Timing Waterfall</strong>#{bars.join}</div>"
121
+ end
122
+ end
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bidi2pdf
4
+ module Bidi
5
+ module NetworkEventFormatters
6
+ require_relative "network_event_formatters/network_event_formatter_utils"
7
+ require_relative "network_event_formatters/network_event_console_formatter"
8
+ require_relative "network_event_formatters/network_event_html_formatter"
9
+ end
10
+ end
11
+ end
@@ -1,15 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "network_event"
4
+ require_relative "network_event_formatters"
4
5
 
5
6
  module Bidi2pdf
6
7
  module Bidi
7
8
  class NetworkEvents
8
- attr_reader :context_id, :events
9
+ attr_reader :context_id, :events, :network_event_formatter
9
10
 
10
11
  def initialize(context_id)
11
12
  @context_id = context_id
12
13
  @events = {}
14
+ @network_event_formatter = NetworkEventFormatters::NetworkEventConsoleFormatter.new
13
15
  end
14
16
 
15
17
  def handle_event(data)
@@ -19,46 +21,73 @@ module Bidi2pdf
19
21
  if event["context"] == context_id
20
22
  handle_response(method, event)
21
23
  else
22
- Bidi2pdf.logger.debug "Ignoring Network event: #{method}, #{context_id}, params: #{event}"
24
+ Bidi2pdf.logger.debug3 "Ignoring Network event: #{method}, #{context_id}, params: #{event}"
23
25
  end
24
26
  rescue StandardError => e
25
27
  Bidi2pdf.logger.error "Error handling network event: #{e.message}"
26
28
  end
27
29
 
28
- # rubocop:disable Metrics/AbcSize
30
+ # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
29
31
  def handle_response(method, event)
30
32
  return unless event && event["request"]
31
33
 
32
34
  request = event["request"]
35
+ response = event["response"]
36
+ http_status_code = response&.dig("status")
37
+ bytes_received = response&.dig("bytesReceived")
33
38
 
34
39
  id = request["request"]
35
40
  url = request["url"]
36
41
  timing = request["timings"]
42
+ http_method = request["method"]
37
43
 
38
44
  timestamp = event["timestamp"]
39
45
 
40
- if method == "network.responseStarted"
41
- events[id] ||= NetworkEvent.new(
42
- id: id,
43
- url: url,
44
- timestamp: timestamp,
45
- timing: timing,
46
- state: method
47
- )
48
- elsif events.key?(id)
49
- events[id].update_state(method, timestamp: timestamp, timing: timing)
50
- else
51
- Bidi2pdf.logger.warn "Received response for unknown request ID: #{id}, URL: #{url}"
46
+ Bidi2pdf.notification_service.instrument("network_event_received.bidi2pdf",
47
+ {
48
+ id: id,
49
+ method: method,
50
+ url: url,
51
+ http_status_code: http_status_code
52
+ }) do |instrumentation_payload|
53
+ if method == "network.beforeRequestSent"
54
+ events[id] ||= NetworkEvent.new(
55
+ id: id,
56
+ url: url,
57
+ timestamp: timestamp,
58
+ timing: timing,
59
+ state: method,
60
+ http_method: http_method
61
+ )
62
+ elsif events.key?(id)
63
+ events[id].update_state(method, timestamp: timestamp, timing: timing, http_status_code: http_status_code, bytes_received: bytes_received)
64
+ else
65
+ Bidi2pdf.logger.warn "Received response for unknown request ID: #{id}, URL: #{url}"
66
+ end
67
+
68
+ instrumentation_payload[:event] = events[id]&.dup
52
69
  end
53
70
  end
54
71
 
55
- # rubocop:enable Metrics/AbcSize
72
+ # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
56
73
 
57
74
  def all_events
58
75
  events.values.sort_by(&:start_timestamp)
59
76
  end
60
77
 
61
- def wait_until_all_finished(timeout: 10, poll_interval: 0.1)
78
+ def log_network_traffic(format: :console)
79
+ format = format.to_sym
80
+
81
+ if format == :console
82
+ NetworkEventFormatters::NetworkEventConsoleFormatter.new.log all_events
83
+ elsif format == :html
84
+ NetworkEventFormatters::NetworkEventHtmlFormatter.new.render(all_events)
85
+ else
86
+ raise ArgumentError, "Unknown network event format: #{format}"
87
+ end
88
+ end
89
+
90
+ def wait_until_network_idle(timeout: 10, poll_interval: 0.01)
62
91
  start_time = Time.now
63
92
 
64
93
  loop do
@@ -7,14 +7,47 @@ require_relative "client"
7
7
  require_relative "browser"
8
8
  require_relative "user_context"
9
9
 
10
+ # Represents a session for managing browser interactions and communication
11
+ # using the Bidi2pdf library. This class handles the setup, configuration,
12
+ # and execution of browser-related workflows, including session creation,
13
+ # WebSocket communication, and browser management.
14
+ #
15
+ # @example Creating and starting a session
16
+ # session = Bidi2pdf::Bidi::Session.new(session_url: "http://example.com/session", headless: true)
17
+ # session.start
18
+ #
19
+ # @example Retrieving user contexts
20
+ # session.user_contexts
21
+ #
22
+ # @example Closing the session
23
+ # session.close
24
+ #
25
+ # @param [String] session_url The URL for the session.
26
+ # @param [Boolean] headless Whether to run the browser in headless mode. Defaults to true.
27
+ # @param [Array<String>] chrome_args Additional Chrome arguments. Defaults to predefined arguments.
10
28
  module Bidi2pdf
11
29
  module Bidi
12
30
  class Session
13
- SUBSCRIBE_EVENTS = %w[log script].freeze
31
+ # Events to subscribe to during the session.
32
+ SUBSCRIBE_EVENTS = %w[script].freeze
33
+
34
+ # Default Chrome arguments for the session.
14
35
  DEFAULT_CHROME_ARGS = %w[--disable-gpu --disable-popup-blocking --disable-hang-monitor].freeze
15
36
 
16
- attr_reader :session_uri, :started, :chrome_args
37
+ # @return [URI] The URI of the session.
38
+ attr_reader :session_uri
39
+
40
+ # @return [Boolean] Whether the session has started.
41
+ attr_reader :started
42
+
43
+ # @return [Array<String>] The Chrome arguments for the session.
44
+ attr_reader :chrome_args
17
45
 
46
+ # Initializes a new session.
47
+ #
48
+ # @param [String] session_url The URL for the session.
49
+ # @param [Boolean] headless Whether to run the browser in headless mode. Defaults to true.
50
+ # @param [Array<String>] chrome_args Additional Chrome arguments. Defaults to predefined arguments.
18
51
  def initialize(session_url:, headless: true, chrome_args: DEFAULT_CHROME_ARGS)
19
52
  @session_uri = URI(session_url)
20
53
  @headless = headless
@@ -22,6 +55,9 @@ module Bidi2pdf
22
55
  @chrome_args = chrome_args.dup
23
56
  end
24
57
 
58
+ # Starts the session and initializes the client.
59
+ #
60
+ # @raise [StandardError] If an error occurs during session start.
25
61
  def start
26
62
  return if started?
27
63
 
@@ -32,41 +68,68 @@ module Bidi2pdf
32
68
  raise e
33
69
  end
34
70
 
71
+ # Returns the WebSocket client for the session.
72
+ #
73
+ # @return [Bidi2pdf::Bidi::Client, nil] The WebSocket client, or nil if the session is not started.
35
74
  def client
36
75
  @client ||= started? ? create_client : nil
37
76
  end
38
77
 
78
+ # Returns the browser instance for the session.
79
+ #
80
+ # @return [Bidi2pdf::Bidi::Browser] The browser instance.
39
81
  def browser
40
82
  @browser ||= create_browser
41
83
  end
42
84
 
85
+ # Closes the session and cleans up resources.
86
+ # rubocop:disable Metrics/AbcSize
43
87
  def close
44
88
  return unless started?
45
89
 
46
90
  2.times do |attempt|
47
- client&.send_cmd_and_wait(Bidi2pdf::Bidi::Commands::SessionEnd.new, timeout: 1) do |response|
48
- Bidi2pdf.logger.info "Session ended: #{response}"
49
-
50
- cleanup
91
+ success = Bidi2pdf.notification_service.instrument("session_close.bidi2pdf", { session_uri: session_uri.to_s, attempt: attempt + 1 }) do |payload|
92
+ client&.send_cmd_and_wait(Bidi2pdf::Bidi::Commands::SessionEnd.new, timeout: 1) do |response|
93
+ payload[:response] = response
94
+ cleanup
95
+ end
96
+
97
+ true
98
+ rescue CmdTimeoutError => e
99
+ payload[:error] = e
100
+ payload[:retry] = attempt < 1 # whether we'll retry again
101
+
102
+ false
51
103
  end
52
- break
53
- rescue CmdTimeoutError
54
- Bidi2pdf.logger.error "Session end command timed out. Retrying... (#{attempt + 1})"
104
+
105
+ break if success
55
106
  end
107
+ ensure
108
+ @started = false
56
109
  end
57
110
 
111
+ # rubocop: enable Metrics/AbcSize
112
+
113
+ # Retrieves user contexts for the session.
58
114
  def user_contexts
59
115
  send_cmd(Bidi2pdf::Bidi::Commands::GetUserContexts.new) { |resp| Bidi2pdf.logger.debug "User contexts: #{resp}" }
60
116
  end
61
117
 
118
+ # Retrieves the status of the session.
62
119
  def status
63
120
  send_cmd(Bidi2pdf::Bidi::Commands::SessionStatus.new) { |resp| Bidi2pdf.logger.info "Session status: #{resp.inspect}" }
64
121
  end
65
122
 
123
+ # Checks if the session has started.
124
+ #
125
+ # @return [Boolean] True if the session has started, false otherwise.
66
126
  def started?
67
127
  @started
68
128
  end
69
129
 
130
+ # Retrieves the WebSocket URL for the session.
131
+ #
132
+ # @return [String] The WebSocket URL.
70
133
  def websocket_url
71
134
  return @websocket_url if @websocket_url
72
135
 
@@ -79,11 +142,18 @@ module Bidi2pdf
79
142
 
80
143
  private
81
144
 
82
- def send_cmd(command, &block)
83
- client&.send_cmd_and_wait(command, &block)
145
+ # Sends a command to the WebSocket client.
146
+ #
147
+ # @param [Object] command The command to send.
148
+ # @yield [response] A block to handle the response.
149
+ def send_cmd(command, &)
150
+ client&.send_cmd_and_wait(command, &)
84
151
  end
85
152
 
86
- # rubocop: disable Metrics/AbcSize
153
+ # Creates a new browser instance.
154
+ #
155
+ # @return [Bidi2pdf::Bidi::Browser] The browser instance.
156
+ # rubocop:disable Metrics/AbcSize
87
157
  def create_browser
88
158
  start
89
159
  client.start
@@ -103,12 +173,18 @@ module Bidi2pdf
103
173
  Bidi::Browser.new(client)
104
174
  end
105
175
 
106
- # rubocop: enable Metrics/AbcSize
176
+ # rubocop:enable Metrics/AbcSize
107
177
 
178
+ # Creates a new WebSocket client.
179
+ #
180
+ # @return [Bidi2pdf::Bidi::Client] The WebSocket client.
108
181
  def create_client
109
182
  Bidi::Client.new(websocket_url).tap(&:start)
110
183
  end
111
184
 
185
+ # Creates a new session and retrieves the WebSocket URL.
186
+ #
187
+ # @return [String] The WebSocket URL.
112
188
  def create_new_session
113
189
  session_data = exec_api_call(session_request)
114
190
  Bidi2pdf.logger.debug "Session data: #{session_data}"
@@ -124,6 +200,9 @@ module Bidi2pdf
124
200
  ws_url
125
201
  end
126
202
 
203
+ # Builds the session request payload.
204
+ #
205
+ # @return [Hash] The session request payload.
127
206
  def session_request
128
207
  session_chrome_args = chrome_args.dup
129
208
  session_chrome_args << "--headless" if @headless
@@ -142,6 +221,10 @@ module Bidi2pdf
142
221
  }
143
222
  end
144
223
 
224
+ # Executes an API call with the given payload.
225
+ #
226
+ # @param [Hash] payload The payload for the API call.
227
+ # @return [Hash] The parsed response data.
145
228
  def exec_api_call(payload)
146
229
  response = Net::HTTP.post(session_uri, payload.to_json, "Content-Type" => "application/json")
147
230
  body = response.body
@@ -158,11 +241,20 @@ module Bidi2pdf
158
241
  build_error(error_type, "#{error_description(error_type)} #{e.message}", e.backtrace)
159
242
  end
160
243
 
244
+ # Logs an API error.
245
+ #
246
+ # @param [String] message The error message.
247
+ # @param [Integer] code The response code.
248
+ # @param [String] body The response body.
161
249
  def log_api_error(message, code, body)
162
250
  Bidi2pdf.logger.error "#{message}. Response code: #{code}"
163
251
  Bidi2pdf.logger.error "Response body: #{body}"
164
252
  end
165
253
 
254
+ # Determines the error category based on the exception.
255
+ #
256
+ # @param [Exception] exception The exception to categorize.
257
+ # @return [String] The error category.
166
258
  def error_category(exception)
167
259
  case exception
168
260
  when Errno::ECONNREFUSED then "Connection refused"
@@ -171,6 +263,10 @@ module Bidi2pdf
171
263
  end
172
264
  end
173
265
 
266
+ # Retrieves the error description for a given error type.
267
+ #
268
+ # @param [String] type The error type.
269
+ # @return [String] The error description.
174
270
  def error_description(type)
175
271
  {
176
272
  "Connection refused" => "Could not connect to the session URL:",
@@ -179,6 +275,12 @@ module Bidi2pdf
179
275
  }[type]
180
276
  end
181
277
 
278
+ # Builds an error response.
279
+ #
280
+ # @param [String] error The error type.
281
+ # @param [String] message The error message.
282
+ # @param [Array<String>, nil] backtrace The error backtrace.
283
+ # @return [Hash] The error response.
182
284
  def build_error(error, message, backtrace = nil)
183
285
  {
184
286
  "value" => {
@@ -189,6 +291,10 @@ module Bidi2pdf
189
291
  }
190
292
  end
191
293
 
294
+ # Handles an error response from the session.
295
+ #
296
+ # @param [Hash] value The error response value.
297
+ # @raise [SessionNotStartedError] If the session could not be started.
192
298
  def handle_error(value)
193
299
  error = value["error"]
194
300
  return unless error
@@ -208,6 +314,7 @@ module Bidi2pdf
208
314
  "Session not started. Check logs for more details. Error: #{error} message: #{msg}"
209
315
  end
210
316
 
317
+ # Cleans up resources associated with the session.
211
318
  def cleanup
212
319
  @client&.close
213
320
  @client = @websocket_url = @browser = nil