bidi2pdf 0.1.1 → 0.1.3

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 (42) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +13 -0
  3. data/CHANGELOG.md +40 -2
  4. data/README.md +10 -0
  5. data/Rakefile +2 -0
  6. data/cliff.toml +138 -0
  7. data/docker/Dockerfile.chromedriver +33 -0
  8. data/docker/docker-compose.yml +7 -0
  9. data/docker/entrypoint.sh +3 -0
  10. data/docker/nginx/default.conf +6 -0
  11. data/lib/bidi2pdf/bidi/add_headers_interceptor.rb +1 -1
  12. data/lib/bidi2pdf/bidi/auth_interceptor.rb +1 -1
  13. data/lib/bidi2pdf/bidi/client.rb +62 -146
  14. data/lib/bidi2pdf/bidi/command_manager.rb +82 -0
  15. data/lib/bidi2pdf/bidi/connection_manager.rb +34 -0
  16. data/lib/bidi2pdf/bidi/session.rb +30 -22
  17. data/lib/bidi2pdf/chromedriver_manager.rb +40 -16
  18. data/lib/bidi2pdf/cli.rb +147 -18
  19. data/lib/bidi2pdf/launcher.rb +19 -6
  20. data/lib/bidi2pdf/version.rb +1 -1
  21. data/sig/bidi2pdf/bidi/add_headers_interceptor.rbs +20 -0
  22. data/sig/bidi2pdf/bidi/auth_interceptor.rbs +17 -0
  23. data/sig/bidi2pdf/bidi/browser.rbs +38 -0
  24. data/sig/bidi2pdf/bidi/browser_tab.rbs +42 -0
  25. data/sig/bidi2pdf/bidi/client.rbs +72 -0
  26. data/sig/bidi2pdf/bidi/event_manager.rbs +29 -0
  27. data/sig/bidi2pdf/bidi/network_event.rbs +51 -0
  28. data/sig/bidi2pdf/bidi/network_events.rbs +55 -0
  29. data/sig/bidi2pdf/bidi/print_parameters_validator.rbs +44 -0
  30. data/sig/bidi2pdf/bidi/session.rbs +52 -0
  31. data/sig/bidi2pdf/bidi/user_context.rbs +50 -0
  32. data/sig/bidi2pdf/bidi/web_socket_dispatcher.rbs +53 -0
  33. data/sig/bidi2pdf/chromedriver_manager.rbs +42 -0
  34. data/sig/bidi2pdf/cli.rbs +21 -0
  35. data/sig/bidi2pdf/launcher.rbs +38 -0
  36. data/sig/bidi2pdf/process_tree.rbs +27 -0
  37. data/sig/bidi2pdf/session_runner.rbs +51 -0
  38. data/sig/bidi2pdf/utils.rbs +5 -0
  39. data/sig/vendor/thor.rbs +13 -0
  40. data/tasks/changelog.rake +29 -0
  41. data/tasks/generate_rbs.rake +64 -0
  42. metadata +48 -8
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a67b40e096dd2b6b73fa65cec7d9d75182b995435c17186d755f89efebb2ab26
4
- data.tar.gz: fde0861d58dd30788aa1b54a24d956d3438bd83cc08a5ec9d22e6750a3e646d6
3
+ metadata.gz: 715f3587349e1680386f0c485f45b91a6096143c07cc8b531ca7e284a9516615
4
+ data.tar.gz: 8914fd60cdb04510070dee6b6d7134a94666584583681e26e830824bbf457059
5
5
  SHA512:
6
- metadata.gz: 52e199c72b902e046140e6d47a9b00a8354e53a88f3af6135f0d7ab587c7d59532a904388e4a62ed4ba29c03609eab3b8c7f3aea0aa131ba8880f8ecb6f9a503
7
- data.tar.gz: a83f7091a455e5c8cb5f2b7e8e621c044b2184c2c6e5a6e11aaa4f4ca2f9879ada89eb7951e7ad78e07a2d473396bb529ce5c4dfc771130a40a1977bb6e60575
6
+ metadata.gz: a8e17273531b0131ef9e8b5454154996dbd340d18cbf16a151478b91234e1e53005e0ffb057e16d540f6870b8915bafbdf03831afc2d9b4e9d861494ca13c38f
7
+ data.tar.gz: d0a4d3b193fc51b681a2e3b54da5cc7cccf05cf578b908144eb7ca5988327f0ee938b07dce88ca826d37889a469e3f32a7da61c68bbb05b5913731aeb0cc9e38
data/.rubocop.yml CHANGED
@@ -45,6 +45,19 @@ Gemspec/DevelopmentDependencies:
45
45
  RSpec/InstanceVariable:
46
46
  Enabled: false
47
47
 
48
+ RSpec/BeforeAfterAll:
49
+ Enabled: false
50
+
51
+ RSpec/SpecFilePathFormat:
52
+ Enabled: true
53
+ Exclude:
54
+ - 'spec/acceptance/**/*_spec.rb'
55
+
56
+ RSpec/DescribeClass:
57
+ Enabled: true
58
+ Exclude:
59
+ - 'spec/acceptance/**/*_spec.rb'
60
+
48
61
  plugins:
49
62
  - rubocop-rake
50
63
  - rubocop-rspec
data/CHANGELOG.md CHANGED
@@ -1,8 +1,46 @@
1
+ <!-- generated by git-cliff start -->
2
+
3
+ # Changelog
4
+
5
+ All notable changes to this project will be documented in this file.
6
+
7
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
8
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
9
+
1
10
  ## [Unreleased]
2
11
 
12
+ [unreleased]: https://github.com/dieter-medium/bidi2pdf/compare/v0.1.2..HEAD
13
+
14
+ <!-- generated by git-cliff end -->
15
+
16
+ ## [0.1.3] - 2025-04-06
17
+
18
+ ### 🐛 Fixed
19
+
20
+ - Improve zombie process detection and termination logic in Chromedriver manager by @dieter-medium
21
+ - Add close method for WebSocket connection and update session close logic. Don't leak threads. by @dieter-medium
22
+
23
+ ## [0.1.2] - 2025-04-05
24
+
25
+ ### 🔄 Changed
26
+
27
+ - Enhance CLI with YAML configuration support and additional options by @dieter-medium
28
+
29
+ ### 🚀 Added
30
+
31
+ - Add CLI tests for rendering and configuration options by @dieter-medium
32
+ - Add RSpec test for Bidi2pdf version number by @dieter-medium
33
+ - Add PDF rendering test with custom print parameters and debug output by @dieter-medium
34
+ - Add be_alive_process matcher and chromedriver manager specs for process management by @dieter-medium
35
+ - Add RBS type signatures for Bidi2pdf classes and modules by @dieter-medium
36
+ - Add build status badge to README by @dieter-medium
37
+ - Add badges for maintainability, gem version, and test coverage in README by @dieter-medium
38
+ - Add Code Climate coverage reporting to CI workflow by @dieter-medium
39
+ - Add support for remote ChromeDriver connections and update Docker setup by @dieter-medium
40
+
3
41
  ## [0.1.1] - 2025-04-01
4
42
 
5
- ### Added
43
+ ### 🚀 Added
6
44
 
7
45
  - Docker Compose setup with Nginx for authentication examples
8
46
  - Sample configurations for different authentication methods:
@@ -10,7 +48,7 @@
10
48
  - Cookie-based authentication
11
49
  - API key header authentication
12
50
 
13
- ### Fixed
51
+ ### 🐛 Fixed
14
52
 
15
53
  - HTTP cookie handling issues
16
54
 
data/README.md CHANGED
@@ -1,3 +1,8 @@
1
+ [![Build Status](https://github.com/dieter-medium/bidi2pdf/actions/workflows/ruby.yml/badge.svg)](https://github.com/dieter-medium/bidi2pdf/blob/main/.github/workflows/ruby.yml)
2
+ [![Maintainability](https://api.codeclimate.com/v1/badges/6425d9893aa3a9ca243e/maintainability)](https://codeclimate.com/github/dieter-medium/bidi2pdf/maintainability)
3
+ [![Gem Version](https://badge.fury.io/rb/bidi2pdf.svg)](https://badge.fury.io/rb/bidi2pdf)
4
+ [![Test Coverage](https://api.codeclimate.com/v1/badges/6425d9893aa3a9ca243e/test_coverage)](https://codeclimate.com/github/dieter-medium/bidi2pdf/test_coverage)
5
+
1
6
  # Bidi2pdf
2
7
 
3
8
  Bidi2pdf is a Ruby gem that generates high-quality PDFs from web pages using Chrome's BiDi (BiDirectional) protocol. It
@@ -94,6 +99,7 @@ docker run -it --rm -v ./output:/reports bidi2pdf \
94
99
  ### Test it with docker compose
95
100
 
96
101
  ```bash
102
+ rake build
97
103
  docker compose -f docker/docker-compose.yml up -d
98
104
 
99
105
  # simple example
@@ -108,6 +114,8 @@ docker compose -f docker/docker-compose.yml exec app bidi2pdf render --url=http:
108
114
  # cookie example
109
115
  docker compose -f docker/docker-compose.yml exec app bidi2pdf render --url=http://nginx/cookie/sample.html --cookie "auth=secret" --wait_window_loaded --wait_network_idle --output /reports/cookie.pdf
110
116
 
117
+ # remote chrome example
118
+ docker compose -f docker/docker-compose.yml exec app bidi2pdf render --url=http://nginx/cookie/sample.html --remote_browser_url http://remote-chrome:3000/session --cookie "auth=secret" --wait_window_loaded --wait_network_idle --output /reports/remote.pdf
111
119
 
112
120
  docker compose -f docker/docker-compose.yml down
113
121
  ```
@@ -126,6 +134,8 @@ docker compose -f docker/docker-compose.yml down
126
134
  | `--wait_window_loaded` | Wait for the window to be fully loaded. You need to set a variable `window.loaded`. See ./spec/fixtures/sample.html |
127
135
  | `--wait_network_idle` | Wait for network to be idle |
128
136
  | `--log_level` | Log level (debug, info, warn, error, fatal) |
137
+ | `--remote_browser_url` | URL of the remote Chrome instance (default: nil) |
138
+ | `--default_timeout` | Default timeout for operations (default: 60 seconds) |
129
139
 
130
140
  ## Development
131
141
 
data/Rakefile CHANGED
@@ -14,6 +14,8 @@ task default: %i[spec rubocop]
14
14
  require "chromedriver/binary"
15
15
  load "chromedriver/Rakefile"
16
16
 
17
+ Dir.glob("tasks/*.rake").each { |r| load r }
18
+
17
19
  desc "Run tests with coverage"
18
20
  task :coverage do
19
21
  ENV["COVERAGE"] = "true"
data/cliff.toml ADDED
@@ -0,0 +1,138 @@
1
+ # git-cliff ~ configuration file
2
+ # https://git-cliff.org/docs/configuration
3
+
4
+ [changelog]
5
+ # template for the changelog header
6
+ header = """
7
+ <!-- generated by git-cliff start -->
8
+ # Changelog\n
9
+ All notable changes to this project will be documented in this file.
10
+
11
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
12
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).\n
13
+ """
14
+ # template for the changelog body
15
+ # https://keats.github.io/tera/docs/#introduction
16
+ body = """
17
+ {%- macro remote_url() -%}
18
+ https://github.com/{{ remote.github.owner }}/{{ remote.github.repo }}
19
+ {%- endmacro -%}
20
+
21
+ {% if version -%}
22
+ ## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }}
23
+ {% else -%}
24
+ ## [Unreleased]
25
+ {% endif -%}
26
+
27
+ {% for group, commits in commits | group_by(attribute="group") %}
28
+ ### {{ group | upper_first }}
29
+ {%- for commit in commits %}
30
+ - {{ commit.message | split(pat="\n") | first | upper_first | trim }}\
31
+ {% if commit.remote.username %} by @{{ commit.remote.username }}{%- endif -%}
32
+ {% if commit.remote.pr_number %} in \
33
+ [#{{ commit.remote.pr_number }}]({{ self::remote_url() }}/pull/{{ commit.remote.pr_number }}) \
34
+ {%- endif -%}
35
+ {% endfor %}
36
+ {% endfor %}
37
+
38
+ {%- if github.contributors | filter(attribute="is_first_time", value=true) | length != 0 %}
39
+ ## New Contributors
40
+ {%- endif -%}
41
+
42
+ {% for contributor in github.contributors | filter(attribute="is_first_time", value=true) %}
43
+ * @{{ contributor.username }} made their first contribution
44
+ {%- if contributor.pr_number %} in \
45
+ [#{{ contributor.pr_number }}]({{ self::remote_url() }}/pull/{{ contributor.pr_number }}) \
46
+ {%- endif %}
47
+ {%- endfor %}\n
48
+ """
49
+ # template for the changelog footer
50
+ footer = """
51
+ {%- macro remote_url() -%}
52
+ https://github.com/{{ remote.github.owner }}/{{ remote.github.repo }}
53
+ {%- endmacro -%}
54
+
55
+ {% for release in releases -%}
56
+ {% if release.version -%}
57
+ {% if release.previous.version -%}
58
+ [{{ release.version | trim_start_matches(pat="v") }}]: \
59
+ {{ self::remote_url() }}/compare/{{ release.previous.version }}..{{ release.version }}
60
+ {% endif -%}
61
+ {% else -%}
62
+ [unreleased]: {{ self::remote_url() }}/compare/{{ release.previous.version }}..HEAD
63
+ {% endif -%}
64
+ {% endfor %}
65
+ <!-- generated by git-cliff end -->
66
+ """
67
+ # remove the leading and trailing whitespace from the templates
68
+ trim = true
69
+
70
+ [git]
71
+ # parse the commits based on https://www.conventionalcommits.org
72
+ conventional_commits = true
73
+ # filter out the commits that are not conventional
74
+ filter_unconventional = false
75
+ # regex for preprocessing the commit messages
76
+ commit_preprocessors = [
77
+ # remove issue numbers from commits
78
+ { pattern = '\((\w+\s)?#([0-9]+)\)', replace = "" },
79
+ ]
80
+ # regex for parsing and grouping commits
81
+ commit_parsers = [
82
+ # ✅ Features (additions)
83
+ { message = "^feat(?:\\([^)]+\\))?!?:", group = "🚀 Added" },
84
+ { message = "^[aA]dd", group = "🚀 Added" },
85
+ { message = "^[sS]upport", group = "🚀 Added" },
86
+
87
+ # ❌ Removals
88
+ { message = "^[rR]emove", group = "🗑️ Removed" },
89
+ { message = "^[dD]elete", group = "🗑️ Removed" },
90
+
91
+ # 🐛 Fixes
92
+ { message = "^fix(?:\\([^)]+\\))?!?:", group = "🐛 Fixed" },
93
+ { message = "^[tT]est", group = "🐛 Fixed" },
94
+ { message = "^[fF]ix", group = "🐛 Fixed" },
95
+
96
+ # 🎨 Refactors
97
+ { message = "^refactor(?:\\([^)]+\\))?!?:", group = "🎨 Refactored" },
98
+
99
+ # ⚡️ Performance
100
+ { message = "^perf(?:\\([^)]+\\))?!?:", group = "⚡️ Performance" },
101
+
102
+ # 📝 Docs
103
+ { message = "^docs(?:\\([^)]+\\))?!?:", group = "📝 Docs" },
104
+
105
+ # 💄 Style (formatting, whitespace, etc.)
106
+ { message = "^style(?:\\([^)]+\\))?!?:", group = "💄 Style" },
107
+
108
+ # 🧪 Tests
109
+ { message = "^test(?:\\([^)]+\\))?!?:", group = "🧪 Tests" },
110
+
111
+ # 🔧 Build
112
+ { message = "^build(?:\\([^)]+\\))?!?:", group = "🔧 Build" },
113
+
114
+ # 🛠️ CI
115
+ { message = "^ci(?:\\([^)]+\\))?!?:", skip = true },
116
+
117
+ # 🧹 Chores (skip)
118
+ { message = "^chore\\(release\\): prepare for", skip = true },
119
+ { message = "^chore\\(deps.*\\)", skip = true },
120
+ { message = "^chore\\(pr\\)", skip = true },
121
+ { message = "^chore\\(pull\\)", skip = true },
122
+ { message = "^chore(?:\\([^)]+\\))?!?:", skip = true },
123
+ { message = "^\\s*chore", skip = true },
124
+
125
+ # ⏪ Reverts
126
+ { message = "^revert(?:\\([^)]+\\))?!?:", group = "⏪ Reverted" },
127
+
128
+ # 🌀 Catch-all (only if nothing else matched)
129
+ { message = "^.*", group = "🔄 Changed" }
130
+ ]
131
+
132
+
133
+ # filter out the commits that are not matched by commit parsers
134
+ filter_commits = false
135
+ # sort the tags topologically
136
+ topo_order = false
137
+ # sort the commits inside sections by oldest/newest order
138
+ sort_commits = "newest"
@@ -0,0 +1,33 @@
1
+ FROM ruby:3.3
2
+
3
+ # Install dependencies
4
+ RUN apt-get update && \
5
+ apt-get install -y \
6
+ chromium \
7
+ libglib2.0-0 \
8
+ libnss3 \
9
+ libxss1 \
10
+ libasound2 \
11
+ libatk-bridge2.0-0 \
12
+ libgtk-3-0 \
13
+ libdrm2 \
14
+ curl \
15
+ unzip \
16
+ xvfb \
17
+ && rm -rf /var/lib/apt/lists/*
18
+
19
+ # Create a non-root user
20
+ RUN groupadd -r appuser && useradd -r -g appuser -m -d /home/appuser appuser
21
+
22
+ COPY ./docker/entrypoint.sh /usr/local/bin/entrypoint.sh
23
+ RUN chmod +x /usr/local/bin/entrypoint.sh
24
+
25
+ # Set working directory
26
+ WORKDIR /app
27
+
28
+ # Switch to non-root user
29
+ USER appuser
30
+
31
+ RUN gem install chromedriver-binary && ruby -e 'require "chromedriver/binary"; puts Chromedriver::Binary::ChromedriverDownloader.update'
32
+
33
+ CMD ["/usr/local/bin/entrypoint.sh"]
@@ -9,6 +9,13 @@ services:
9
9
  - ./nginx/htpasswd:/etc/nginx/conf.d/.htpasswd
10
10
  - ../spec/fixtures:/var/www/html
11
11
 
12
+ remote-chrome:
13
+ build:
14
+ context: ..
15
+ dockerfile: docker/Dockerfile.chromedriver
16
+ ports:
17
+ - "9092:3000"
18
+
12
19
  app:
13
20
  build:
14
21
  context: ..
@@ -0,0 +1,3 @@
1
+ #!/bin/bash
2
+
3
+ /home/appuser/.webdrivers/chromedriver --port=3000 --headless --whitelisted-ips="" --allowed-origins="*" --disable-dev-shm-usage --verbose
@@ -39,4 +39,10 @@ server {
39
39
  return 403;
40
40
  }
41
41
  }
42
+
43
+ location /nginx_status {
44
+ stub_status on;
45
+
46
+ access_log off;
47
+ }
42
48
  }
@@ -5,7 +5,7 @@ module Bidi2pdf
5
5
  class AddHeadersInterceptor
6
6
  attr_reader :id, :headers
7
7
 
8
- def initialize(id, headers, client)
8
+ def initialize(id, headers:, client:)
9
9
  @id = id
10
10
  @client = client
11
11
  @headers = headers.map do |header|
@@ -5,7 +5,7 @@ module Bidi2pdf
5
5
  class AuthInterceptor
6
6
  attr_reader :id, :username, :password, :network_ids
7
7
 
8
- def initialize(id, username, password, client)
8
+ def initialize(id, username:, password:, client:)
9
9
  @id = id
10
10
  @client = client
11
11
  @username = username
@@ -6,6 +6,8 @@ require "websocket-client-simple"
6
6
  require_relative "web_socket_dispatcher"
7
7
  require_relative "add_headers_interceptor"
8
8
  require_relative "auth_interceptor"
9
+ require_relative "command_manager"
10
+ require_relative "connection_manager"
9
11
 
10
12
  module Bidi2pdf
11
13
  module Bidi
@@ -16,14 +18,6 @@ module Bidi2pdf
16
18
 
17
19
  def initialize(ws_url)
18
20
  @ws_url = ws_url
19
- @id = 0
20
- @pending_responses = {}
21
-
22
- @connected = false
23
- @connection_mutex = Mutex.new
24
- @send_cmd_mutex = Mutex.new
25
- @connection_cv = ConditionVariable.new
26
-
27
21
  @started = false
28
22
  end
29
23
 
@@ -31,192 +25,114 @@ module Bidi2pdf
31
25
  return @socket if started?
32
26
 
33
27
  @socket = WebSocket::Client::Simple.connect(ws_url)
34
- @dispatcher = WebSocketDispatcher.new(@socket)
35
28
 
36
- @dispatcher.on_open { handle_open }
37
- @dispatcher.on_message { |data| handle_response_to_cmd(data) }
29
+ @connection_manager = ConnectionManager.new(logger: Bidi2pdf.logger)
30
+ @command_manager = CommandManager.new(@socket, logger: Bidi2pdf.logger)
38
31
 
39
- @dispatcher.start_listening
32
+ dispatcher.on_open { @connection_manager.mark_connected }
33
+ dispatcher.on_message { |data| handle_response_to_cmd(data) }
40
34
 
35
+ dispatcher.start_listening
41
36
  @started = true
42
37
 
43
38
  @socket
44
39
  end
45
40
 
46
- def started?
47
- @started
48
- end
41
+ def started? = @started
49
42
 
50
43
  def wait_until_open(timeout: Bidi2pdf.default_timeout)
51
- @connection_mutex.synchronize do
52
- unless @connected
53
- Bidi2pdf.logger.debug "Waiting for WebSocket connection to open"
54
- @connection_cv.wait(@connection_mutex, timeout)
55
- end
56
- end
57
-
58
- raise "WebSocket connection did not open in time" unless @connected
59
-
60
- Bidi2pdf.logger.debug "WebSocket connection is open"
44
+ @connection_manager.wait_until_open(timeout: timeout)
61
45
  end
62
46
 
63
47
  def send_cmd(method, params = {})
64
- next_id.tap do |cmd_id|
65
- payload = {
66
- id: cmd_id,
67
- method: method,
68
- params: params
69
- }
70
-
71
- Bidi2pdf.logger.debug "Sending command: #{redact_sensitive_fields(payload).inspect}"
72
-
73
- @socket.send(payload.to_json)
74
- end
48
+ @command_manager.send_cmd(method, params)
75
49
  end
76
50
 
77
- # rubocop:disable Metrics/AbcSize
78
- def send_cmd_and_wait(method, params = {}, timeout: Bidi2pdf.default_timeout)
51
+ def send_cmd_and_wait(method, params = {}, timeout: Bidi2pdf.default_timeout, &block)
79
52
  timed("Command #{method}") do
80
- id = send_cmd(method, params)
81
- queue = @pending_responses[id]
82
-
83
- begin
84
- response = queue.pop(true)
85
- rescue ThreadError
86
- response = queue.pop(timeout: timeout)
87
- end
88
-
89
- if response.nil?
90
- # rubocop:disable Layout/LineLength
91
- Bidi2pdf.logger.error "Timeout waiting for response to command #{id}, cmd: #{method}, params: #{redact_sensitive_fields(params).inspect}"
92
- # rubocop:enable Layout/LineLength
93
-
94
- raise "Timeout waiting for response to command ID #{id}"
95
- end
96
-
97
- raise "Error response: #{response["error"]}" if response["error"]
98
-
99
- result = response
100
-
101
- result = yield response if block_given?
102
-
103
- result
104
- ensure
105
- @pending_responses.delete(id)
53
+ @command_manager.send_cmd_and_wait(method, params, timeout: timeout, &block)
106
54
  end
107
55
  end
108
56
 
109
- # rubocop:enable Metrics/AbcSize
110
-
111
- # Event API for external consumers
112
- def on_message(&block) = @dispatcher.on_message(&block)
57
+ def on_message(&block) = dispatcher.on_message(&block)
113
58
 
114
- def on_open(&block) = @dispatcher.on_open(&block)
59
+ def on_open(&block) = dispatcher.on_open(&block)
115
60
 
116
- def on_close(&block) = @dispatcher.on_close(&block)
61
+ def on_close(&block) = dispatcher.on_close(&block)
117
62
 
118
- def on_error(&block) = @dispatcher.on_error(&block)
63
+ def on_error(&block) = dispatcher.on_error(&block)
119
64
 
120
65
  def on_event(*names, &block)
121
- names.each do |name|
122
- @dispatcher.on_event(name, &block)
123
- end
124
-
125
- send_cmd "session.subscribe", { events: names } if names.any?
66
+ names.each { |name| dispatcher.on_event(name, &block) }
67
+ send_cmd("session.subscribe", { events: names }) if names.any?
126
68
  end
127
69
 
128
- def remove_message_listener(block) = @dispatcher.remove_message_listener(block)
70
+ def remove_message_listener(block) = dispatcher.remove_message_listener(block)
129
71
 
130
72
  def remove_event_listener(*names, &block)
131
- names.each do |event_name|
132
- @dispatcher.remove_event_listener(event_name, block)
133
- end
73
+ names.each { |event_name| dispatcher.remove_event_listener(event_name, block) }
134
74
  end
135
75
 
136
- def add_headers_interceptor(
137
- context:,
138
- url_patterns:,
139
- headers:
140
- )
141
- send_cmd_and_wait("network.addIntercept", {
76
+ def add_headers_interceptor(context:, url_patterns:, headers:)
77
+ add_interceptor(
142
78
  context: context,
143
- phases: ["beforeRequestSent"],
144
- urlPatterns: url_patterns
145
- }) do |response|
146
- id = response["result"]["intercept"]
147
- Bidi2pdf.logger.debug "Interceptor added: #{id}"
148
-
149
- AddHeadersInterceptor.new(id, headers, self).tap do |interceptor|
150
- on_event "network.beforeRequestSent", &interceptor.method(:handle_event)
151
- end
152
- end
79
+ url_patterns: url_patterns,
80
+ phase: "beforeRequestSent",
81
+ event: "network.beforeRequestSent",
82
+ interceptor_class: AddHeadersInterceptor,
83
+ extra_args: { headers: headers }
84
+ )
153
85
  end
154
86
 
155
- def add_auth_interceptor(
156
- context:,
157
- url_patterns:,
158
- username:,
159
- password:
160
- )
161
- send_cmd_and_wait("network.addIntercept", {
87
+ def add_auth_interceptor(context:, url_patterns:, username:, password:)
88
+ add_interceptor(
162
89
  context: context,
163
- phases: ["authRequired"],
164
- urlPatterns: url_patterns
165
- }) do |response|
166
- id = response["result"]["intercept"]
167
- Bidi2pdf.logger.debug "Interceptor added: #{id}"
168
-
169
- AuthInterceptor.new(id, username, password, self).tap do |interceptor|
170
- on_event "network.authRequired", &interceptor.method(:handle_event)
171
- end
172
- end
90
+ url_patterns: url_patterns,
91
+ phase: "authRequired",
92
+ event: "network.authRequired",
93
+ interceptor_class: AuthInterceptor,
94
+ extra_args: { username: username, password: password }
95
+ )
173
96
  end
174
97
 
175
- private
176
-
177
- def next_id
178
- cmd_id = nil
179
-
180
- @send_cmd_mutex.synchronize do
181
- @id += 1
182
- cmd_id = @id
183
- @pending_responses[cmd_id] = Queue.new
184
- end
98
+ def close
99
+ return unless @socket
185
100
 
186
- cmd_id
101
+ Bidi2pdf.logger.debug "Closing WebSocket connection"
102
+ @socket&.close
103
+ @socket = nil
104
+ @started = false
187
105
  end
188
106
 
189
- def handle_open
190
- @connection_mutex.synchronize do
191
- @connected = true
192
- @connection_cv.broadcast
193
- end
107
+ private
108
+
109
+ def dispatcher
110
+ @dispatcher ||= WebSocketDispatcher.new(@socket)
194
111
  end
195
112
 
196
113
  def handle_response_to_cmd(data)
197
- if (id = data["id"]) && @pending_responses.key?(id)
198
- @pending_responses[id]&.push(data)
199
- elsif (data = data["error"])
200
- Bidi2pdf.logger.error "Error response: #{data.inspect}"
114
+ handled = @command_manager.handle_response(data)
115
+ return if handled
116
+
117
+ if data["error"]
118
+ Bidi2pdf.logger.error "Error response: #{data["error"].inspect}"
201
119
  else
202
120
  Bidi2pdf.logger.warn "Unknown response: #{data.inspect}"
203
121
  end
204
122
  end
205
123
 
206
- def redact_sensitive_fields(obj, sensitive_keys = %w[value token password authorization username])
207
- case obj
208
- when Hash
209
- obj.each_with_object({}) do |(k, v), result|
210
- result[k] = if sensitive_keys.include?(k.to_s.downcase)
211
- "[REDACTED]"
212
- else
213
- redact_sensitive_fields(v, sensitive_keys)
214
- end
124
+ def add_interceptor(context:, url_patterns:, phase:, event:, interceptor_class:, extra_args: {})
125
+ send_cmd_and_wait("network.addIntercept", {
126
+ context: context,
127
+ phases: [phase],
128
+ urlPatterns: url_patterns
129
+ }) do |response|
130
+ id = response["result"]["intercept"]
131
+ Bidi2pdf.logger.debug "Interceptor added: #{id}"
132
+
133
+ interceptor_class.new(id, **extra_args, client: self).tap do |interceptor|
134
+ on_event(event, &interceptor.method(:handle_event))
215
135
  end
216
- when Array
217
- obj.map { |item| redact_sensitive_fields(item, sensitive_keys) }
218
- else
219
- obj
220
136
  end
221
137
  end
222
138
  end