ask-web-fetch 0.5.1 → 0.5.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0f49a52ecb57c57ee29ce475ad791f70d30fe52946c94c6bd6842dcccc477392
4
- data.tar.gz: b67e97adb54db5b6a775d1ece43df51c0e5443a6ef9e2cd3a17d547af5f7d1dc
3
+ metadata.gz: ed798fddd5578a39814ea41425937d4e539827dae6c66a8e9bc1621007f5dcda
4
+ data.tar.gz: 698add682f0ce4f056acfee23130748bb6d359acb8004a07efc6b862dcc462da
5
5
  SHA512:
6
- metadata.gz: 28dd03ea3e7aae930925ea2d582ff074beba7d3030d762830024e41506a23fd671bb166fa5809417a824e8d2c1729679d00f3d710fa82de05b2ca41038dd39de
7
- data.tar.gz: 84815a2d6504c81df9d81aab2fd679d5bb334f64893b05264cc4185be0e7d7439f357461494c65811cbdb16e0e4548b0b2db290f52b747412678eb953c5f40b3
6
+ metadata.gz: 0aa82e97aef13e08d736024d4927a96a72a554ec1cda21292d501884787ad82e756c51522a179ebfd81bdbcd5baadef6965254957613044e088e511accdfb4f1
7
+ data.tar.gz: e5d46eef13701008e7c4557675d372b6c4776f38f19b75be4559943b675def5d9370006cb9ef10416a7b70f6a3897f54b7c14f4afff409afd590dee0fa068ac7
@@ -50,6 +50,9 @@ module Ask
50
50
  @session = @client.session(
51
51
  @client.command('Target.attachToTarget', targetId: @target_id, flatten: true)['sessionId']
52
52
  )
53
+ # Subscribed BEFORE any navigation so the idle wait sees the
54
+ # page's real load traffic (lazy SPAs render in waves).
55
+ @network = Network.new(self)
53
56
  end
54
57
 
55
58
  # Navigates and does not return until the document has actually
@@ -65,7 +68,6 @@ module Ask
65
68
 
66
69
  wait_for_load
67
70
  end
68
-
69
71
  def title
70
72
  evaluate('document.title')
71
73
  end
@@ -78,11 +80,11 @@ module Ask
78
80
  evaluate('document.documentElement.outerHTML') || ''
79
81
  end
80
82
 
81
- # Minimal network facade for the Browser backend. The status comes
82
- # from the Navigation Timing API (the main document's HTTP status);
83
- # attached pages need no idle wait — they only exist once loaded.
83
+ # Minimal network facade for the Browser backend: status from the
84
+ # Navigation Timing API, idle from CDP Network events (subscribed
85
+ # in the constructor, before navigation).
84
86
  def network
85
- @network ||= Network.new(self)
87
+ @network
86
88
  end
87
89
 
88
90
  def close
@@ -122,10 +124,22 @@ module Ask
122
124
  nil
123
125
  end
124
126
 
125
- # Status + no-op idle for the Browser backend's fetch flow.
127
+ # Real network-idle detection for the attached browser, via CDP's
128
+ # Network domain events. The launched-browser path gets idle
129
+ # waiting from Ferrum's network; the attached path previously
130
+ # skipped it entirely (a no-op), which read lazy-loading SPAs too
131
+ # early — reddit's post list renders in waves, and the fetch saw
132
+ # only the first 2.6k of an 8k+ body. Tracks requestStarted /
133
+ # requestFinished events on the page session and waits until the
134
+ # network has been quiet for +duration+, capped at +timeout+.
126
135
  class Network
127
136
  def initialize(page)
128
137
  @page = page
138
+ @session = page.instance_variable_get(:@session)
139
+ @in_flight = 0
140
+ @quiet_since = Process.clock_gettime(Process::CLOCK_MONOTONIC)
141
+ @mutex = Mutex.new
142
+ subscribe
129
143
  end
130
144
 
131
145
  def status
@@ -137,7 +151,39 @@ module Ask
137
151
  0
138
152
  end
139
153
 
140
- def wait_for_idle(*)
154
+ def wait_for_idle(duration:, timeout:)
155
+ deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout
156
+ loop do
157
+ quiet = @mutex.synchronize { @in_flight.zero? }
158
+ if quiet && Process.clock_gettime(Process::CLOCK_MONOTONIC) - @quiet_since >= duration
159
+ return
160
+ end
161
+ raise Ferrum::TimeoutError, "network did not go idle within #{timeout}s" if Process.clock_gettime(Process::CLOCK_MONOTONIC) >= deadline
162
+
163
+ sleep 0.1
164
+ end
165
+ end
166
+
167
+ private
168
+
169
+ # CDP events arrive on the session's message loop; Ferrum's
170
+ # session dispatches them to subscribed handlers. Track the
171
+ # request lifecycle so "quiet" means no request has been in
172
+ # flight for the duration — the signal lazy SPAs settle on.
173
+ # Defensive: a minimal/mock session without event support
174
+ # (some test doubles) just skips subscription — the load-wait
175
+ # in go_to is still the floor.
176
+ def subscribe
177
+ return unless @session.respond_to?(:on)
178
+
179
+ @session.on('Network.requestWillBeSent') { @mutex.synchronize { @in_flight += 1 } }
180
+ @session.on('Network.responseReceived') { @mutex.synchronize { @quiet_since = Process.clock_gettime(Process::CLOCK_MONOTONIC) } }
181
+ @session.on('Network.loadingFinished') { @mutex.synchronize { @in_flight -= 1 if @in_flight.positive? } }
182
+ @session.on('Network.loadingFailed') { @mutex.synchronize { @in_flight -= 1 if @in_flight.positive? } }
183
+ @session.command('Network.enable')
184
+ rescue Ferrum::Error
185
+ # Some attached browsers reject Network.enable (unlikely);
186
+ # fall back to the load-wait already done in go_to.
141
187
  nil
142
188
  end
143
189
  end
@@ -48,17 +48,24 @@ module Ask
48
48
 
49
49
  # One pooled session per thread — httpx sessions are not thread-safe,
50
50
  # and a crawl worker thread reusing its session across every page it
51
- # fetches is what makes the pooling pay off. Sessions idle-close
52
- # themselves after keep-alive timeout, so nothing to reap.
51
+ # fetches is what keeps timeouts and retries configured once.
52
+ # Sessions idle-close themselves after keep-alive timeout, so nothing
53
+ # to reap.
53
54
  def self.session
54
55
  Thread.current[SESSION_KEY] ||= build_session
55
56
  end
56
57
 
57
58
  def self.build_session
58
- # :persistent is what makes the pooling real — without it httpx
59
- # opens a fresh connection per request. It loads fiber_concurrency
60
- # and the retries plugin internally.
61
- HTTPX.plugin(:persistent).with(
59
+ # Deliberately NOT :persistent: in httpx 1.8.1 that plugin wedges
60
+ # inside the selector loop when a session that already holds a
61
+ # pooled connection opens one to a NEW host — the operation
62
+ # timeout never fires and the fetch hangs forever (reproduced in
63
+ # plain Ruby: example.com then nytimes.com on one session).
64
+ # Without it httpx opens a fresh connection per host, which for a
65
+ # crawler hitting mostly-distinct hosts costs one TLS handshake
66
+ # per page and never hangs. Retries are explicit so the transient
67
+ # backoff that :persistent loaded internally is kept.
68
+ HTTPX.plugin(:retries).with(
62
69
  timeout: {
63
70
  connect_timeout: CONNECT_TIMEOUT,
64
71
  read_timeout: READ_TIMEOUT,
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Ask
4
4
  module WebFetch
5
- VERSION = '0.5.1'
5
+ VERSION = '0.5.3'
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ask-web-fetch
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.1
4
+ version: 0.5.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kaka Ruto