puppeteer-ruby 0.45.6 → 0.50.0.alpha5

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 (98) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +1 -3
  3. data/AGENTS.md +169 -0
  4. data/CLAUDE/README.md +41 -0
  5. data/CLAUDE/architecture.md +253 -0
  6. data/CLAUDE/cdp_protocol.md +230 -0
  7. data/CLAUDE/concurrency.md +216 -0
  8. data/CLAUDE/porting_puppeteer.md +575 -0
  9. data/CLAUDE/rbs_type_checking.md +101 -0
  10. data/CLAUDE/spec_migration_plans.md +1041 -0
  11. data/CLAUDE/testing.md +278 -0
  12. data/CLAUDE.md +242 -0
  13. data/README.md +8 -0
  14. data/Rakefile +7 -0
  15. data/Steepfile +28 -0
  16. data/docs/api_coverage.md +105 -56
  17. data/lib/puppeteer/aria_query_handler.rb +3 -2
  18. data/lib/puppeteer/async_utils.rb +214 -0
  19. data/lib/puppeteer/browser.rb +98 -56
  20. data/lib/puppeteer/browser_connector.rb +18 -3
  21. data/lib/puppeteer/browser_context.rb +196 -3
  22. data/lib/puppeteer/browser_runner.rb +18 -10
  23. data/lib/puppeteer/cdp_session.rb +67 -23
  24. data/lib/puppeteer/chrome_target_manager.rb +65 -40
  25. data/lib/puppeteer/connection.rb +55 -36
  26. data/lib/puppeteer/console_message.rb +9 -1
  27. data/lib/puppeteer/console_patch.rb +47 -0
  28. data/lib/puppeteer/css_coverage.rb +5 -3
  29. data/lib/puppeteer/custom_query_handler.rb +80 -33
  30. data/lib/puppeteer/define_async_method.rb +31 -37
  31. data/lib/puppeteer/dialog.rb +47 -14
  32. data/lib/puppeteer/element_handle.rb +231 -62
  33. data/lib/puppeteer/emulation_manager.rb +1 -1
  34. data/lib/puppeteer/env.rb +1 -1
  35. data/lib/puppeteer/errors.rb +25 -2
  36. data/lib/puppeteer/event_callbackable.rb +15 -0
  37. data/lib/puppeteer/events.rb +4 -0
  38. data/lib/puppeteer/execution_context.rb +148 -3
  39. data/lib/puppeteer/file_chooser.rb +6 -0
  40. data/lib/puppeteer/frame.rb +162 -91
  41. data/lib/puppeteer/frame_manager.rb +69 -48
  42. data/lib/puppeteer/http_request.rb +114 -38
  43. data/lib/puppeteer/http_response.rb +24 -7
  44. data/lib/puppeteer/isolated_world.rb +64 -41
  45. data/lib/puppeteer/js_coverage.rb +5 -3
  46. data/lib/puppeteer/js_handle.rb +58 -16
  47. data/lib/puppeteer/keyboard.rb +30 -17
  48. data/lib/puppeteer/launcher/browser_options.rb +3 -1
  49. data/lib/puppeteer/launcher/chrome.rb +8 -5
  50. data/lib/puppeteer/launcher/launch_options.rb +7 -2
  51. data/lib/puppeteer/launcher.rb +4 -8
  52. data/lib/puppeteer/lifecycle_watcher.rb +38 -22
  53. data/lib/puppeteer/mouse.rb +273 -64
  54. data/lib/puppeteer/network_event_manager.rb +7 -0
  55. data/lib/puppeteer/network_manager.rb +393 -112
  56. data/lib/puppeteer/page/screenshot_task_queue.rb +14 -4
  57. data/lib/puppeteer/page.rb +568 -226
  58. data/lib/puppeteer/puppeteer.rb +171 -64
  59. data/lib/puppeteer/query_handler_manager.rb +112 -16
  60. data/lib/puppeteer/reactor_runner.rb +247 -0
  61. data/lib/puppeteer/remote_object.rb +127 -47
  62. data/lib/puppeteer/target.rb +74 -27
  63. data/lib/puppeteer/task_manager.rb +3 -1
  64. data/lib/puppeteer/timeout_helper.rb +6 -10
  65. data/lib/puppeteer/touch_handle.rb +39 -0
  66. data/lib/puppeteer/touch_screen.rb +72 -22
  67. data/lib/puppeteer/tracing.rb +3 -3
  68. data/lib/puppeteer/version.rb +1 -1
  69. data/lib/puppeteer/wait_task.rb +264 -101
  70. data/lib/puppeteer/web_socket.rb +2 -2
  71. data/lib/puppeteer/web_socket_transport.rb +91 -27
  72. data/lib/puppeteer/web_worker.rb +175 -0
  73. data/lib/puppeteer.rb +20 -4
  74. data/puppeteer-ruby.gemspec +15 -11
  75. data/sig/_external.rbs +8 -0
  76. data/sig/_supplementary.rbs +314 -0
  77. data/sig/puppeteer/browser.rbs +166 -0
  78. data/sig/puppeteer/cdp_session.rbs +64 -0
  79. data/sig/puppeteer/dialog.rbs +41 -0
  80. data/sig/puppeteer/element_handle.rbs +305 -0
  81. data/sig/puppeteer/execution_context.rbs +87 -0
  82. data/sig/puppeteer/frame.rbs +226 -0
  83. data/sig/puppeteer/http_request.rbs +214 -0
  84. data/sig/puppeteer/http_response.rbs +89 -0
  85. data/sig/puppeteer/js_handle.rbs +64 -0
  86. data/sig/puppeteer/keyboard.rbs +40 -0
  87. data/sig/puppeteer/mouse.rbs +113 -0
  88. data/sig/puppeteer/page.rbs +515 -0
  89. data/sig/puppeteer/puppeteer.rbs +98 -0
  90. data/sig/puppeteer/remote_object.rbs +78 -0
  91. data/sig/puppeteer/touch_handle.rbs +21 -0
  92. data/sig/puppeteer/touch_screen.rbs +35 -0
  93. data/sig/puppeteer/web_worker.rbs +83 -0
  94. metadata +116 -45
  95. data/CHANGELOG.md +0 -397
  96. data/lib/puppeteer/concurrent_ruby_utils.rb +0 -81
  97. data/lib/puppeteer/firefox_target_manager.rb +0 -157
  98. data/lib/puppeteer/launcher/firefox.rb +0 -453
data/docs/api_coverage.md CHANGED
@@ -1,22 +1,14 @@
1
1
  # API coverages
2
- - Puppeteer version: v19.5.0
3
- - puppeteer-ruby version: 0.45.6
2
+ - Puppeteer version: v24.35.0
3
+ - puppeteer-ruby version: 0.50.0.alpha5
4
4
 
5
5
  ## Puppeteer
6
6
 
7
- * ~~clearCustomQueryHandlers~~
8
7
  * connect
9
- * ~~createBrowserFetcher~~
10
- * ~~customQueryHandlerNames~~
11
8
  * defaultArgs => `#default_args`
12
- * devices
13
- * ~~errors~~
14
9
  * executablePath => `#executable_path`
15
10
  * launch
16
- * networkConditions => `#network_conditions`
17
11
  * ~~puppeteer~~
18
- * registerCustomQueryHandler => `#register_custom_query_handler`
19
- * ~~unregisterCustomQueryHandler~~
20
12
 
21
13
  ## ~~Accessibility~~
22
14
 
@@ -24,17 +16,28 @@
24
16
 
25
17
  ## Browser
26
18
 
19
+ * ~~addScreen~~
27
20
  * browserContexts => `#browser_contexts`
28
21
  * close
29
- * createIncognitoBrowserContext => `#create_incognito_browser_context`
22
+ * ~~cookies~~
23
+ * createBrowserContext => `#create_browser_context`
30
24
  * defaultBrowserContext => `#default_browser_context`
25
+ * ~~deleteCookie~~
26
+ * ~~deleteMatchingCookies~~
31
27
  * disconnect
28
+ * ~~getWindowBounds~~
29
+ * ~~installExtension~~
32
30
  * isConnected => `#connected?`
33
31
  * newPage => `#new_page`
34
32
  * pages
35
33
  * process
34
+ * ~~removeScreen~~
35
+ * ~~screens~~
36
+ * ~~setCookie~~
37
+ * ~~setWindowBounds~~
36
38
  * target
37
39
  * targets
40
+ * ~~uninstallExtension~~
38
41
  * userAgent => `#user_agent`
39
42
  * version
40
43
  * waitForTarget => `#wait_for_target`
@@ -45,23 +48,21 @@
45
48
  * browser
46
49
  * clearPermissionOverrides => `#clear_permission_overrides`
47
50
  * close
48
- * isIncognito => `#incognito?`
51
+ * cookies
52
+ * deleteCookie => `#delete_cookie`
53
+ * deleteMatchingCookies => `#delete_matching_cookies`
49
54
  * newPage => `#new_page`
50
55
  * overridePermissions => `#override_permissions`
51
56
  * pages
57
+ * setCookie => `#set_cookie`
52
58
  * targets
53
59
  * waitForTarget => `#wait_for_target`
54
60
 
55
- ## ~~BrowserFetcher~~
61
+ ## ~~BrowserLauncher~~
56
62
 
57
- * ~~canDownload~~
58
- * ~~download~~
59
- * ~~host~~
60
- * ~~localRevisions~~
61
- * ~~platform~~
62
- * ~~product~~
63
- * ~~remove~~
64
- * ~~revisionInfo~~
63
+ * ~~defaultArgs~~
64
+ * ~~executablePath~~
65
+ * ~~launch~~
65
66
 
66
67
  ## CDPSession
67
68
 
@@ -79,6 +80,9 @@
79
80
  * session
80
81
  * url
81
82
 
83
+ ## ~~ConnectionClosedError~~
84
+
85
+
82
86
  ## ConsoleMessage
83
87
 
84
88
  * args
@@ -99,8 +103,11 @@
99
103
  * start
100
104
  * stop
101
105
 
102
- ## ~~CustomError~~
106
+ ## ~~DeviceRequestPrompt~~
103
107
 
108
+ * ~~cancel~~
109
+ * ~~select~~
110
+ * ~~waitForDevice~~
104
111
 
105
112
  ## Dialog
106
113
 
@@ -116,8 +123,9 @@
116
123
  * $$ => `#query_selector_all`
117
124
  * $$eval => `#eval_on_selector_all`
118
125
  * $eval => `#eval_on_selector`
119
- * $x => `#Sx`
120
- * asElement => `#as_element`
126
+ * ~~asLocator~~
127
+ * ~~autofill~~
128
+ * ~~backendNodeId~~
121
129
  * boundingBox => `#bounding_box`
122
130
  * boxModel => `#box_model`
123
131
  * click
@@ -130,27 +138,36 @@
130
138
  * drop
131
139
  * focus
132
140
  * hover
141
+ * isHidden => `#hidden?`
133
142
  * isIntersectingViewport => `#intersecting_viewport?`
143
+ * isVisible => `#visible?`
134
144
  * press
135
145
  * screenshot
146
+ * ~~scrollIntoView~~
136
147
  * select
137
148
  * tap
138
149
  * toElement => `#to_element`
150
+ * touchEnd => `#touch_end`
151
+ * touchMove => `#touch_move`
152
+ * touchStart => `#touch_start`
139
153
  * type => `#type_text`
140
154
  * uploadFile => `#upload_file`
141
155
  * waitForSelector => `#wait_for_selector`
142
- * waitForXPath => `#wait_for_xpath`
143
156
 
144
157
  ## ~~EventEmitter~~
145
158
 
146
- * ~~addListener~~
147
159
  * ~~emit~~
148
160
  * ~~listenerCount~~
149
161
  * ~~off~~
150
162
  * ~~on~~
151
163
  * ~~once~~
152
164
  * ~~removeAllListeners~~
153
- * ~~removeListener~~
165
+
166
+ ## ~~ExtensionTransport~~
167
+
168
+ * ~~close~~
169
+ * ~~connectTab~~
170
+ * ~~send~~
154
171
 
155
172
  ## FileChooser
156
173
 
@@ -164,20 +181,19 @@
164
181
  * $$ => `#query_selector_all`
165
182
  * $$eval => `#eval_on_selector_all`
166
183
  * $eval => `#eval_on_selector`
167
- * $x => `#Sx`
168
184
  * addScriptTag => `#add_script_tag`
169
185
  * addStyleTag => `#add_style_tag`
170
- * addStyleTag => `#add_style_tag`
171
186
  * childFrames => `#child_frames`
172
187
  * click
173
188
  * content
174
189
  * evaluate
175
190
  * evaluateHandle => `#evaluate_handle`
176
191
  * focus
192
+ * frameElement => `#frame_element`
177
193
  * goto
178
194
  * hover
179
195
  * isDetached => `#detached?`
180
- * isOOPFrame => `#oop_frame?`
196
+ * ~~locator~~
181
197
  * name
182
198
  * page
183
199
  * parentFrame => `#parent_frame`
@@ -190,8 +206,6 @@
190
206
  * waitForFunction => `#wait_for_function`
191
207
  * waitForNavigation => `#wait_for_navigation`
192
208
  * waitForSelector => `#wait_for_selector`
193
- * waitForTimeout => `#wait_for_timeout`
194
- * waitForXPath => `#wait_for_xpath`
195
209
 
196
210
  ## HTTPRequest
197
211
 
@@ -201,8 +215,10 @@
201
215
  * continueRequestOverrides => `#continue_request_overrides`
202
216
  * enqueueInterceptAction => `#enqueue_intercept_action`
203
217
  * failure
218
+ * fetchPostData => `#fetch_post_data`
204
219
  * finalizeInterceptions => `#finalize_interceptions`
205
220
  * frame
221
+ * ~~hasPostData~~
206
222
  * headers
207
223
  * initiator
208
224
  * interceptResolutionState => `#intercept_resolution_state`
@@ -220,6 +236,7 @@
220
236
  ## HTTPResponse
221
237
 
222
238
  * buffer
239
+ * ~~content~~
223
240
  * frame
224
241
  * ~~fromCache~~
225
242
  * ~~fromServiceWorker~~
@@ -232,7 +249,7 @@
232
249
  * status
233
250
  * statusText => `#status_text`
234
251
  * text
235
- * ~~timing~~
252
+ * timing
236
253
  * url
237
254
 
238
255
  ## JSCoverage
@@ -248,10 +265,9 @@
248
265
  * evaluateHandle => `#evaluate_handle`
249
266
  * getProperties => `#properties`
250
267
  * getProperty => `#[]`
251
- * getProperty => `#[]`
252
268
  * jsonValue => `#json_value`
253
269
  * remoteObject => `#remote_object`
254
- * ~~toString~~
270
+ * toString => `#to_s`
255
271
 
256
272
  ## Keyboard
257
273
 
@@ -261,6 +277,24 @@
261
277
  * type => `#type_text`
262
278
  * up
263
279
 
280
+ ## ~~Locator~~
281
+
282
+ * ~~click~~
283
+ * ~~clone~~
284
+ * ~~fill~~
285
+ * ~~filter~~
286
+ * ~~hover~~
287
+ * ~~map~~
288
+ * ~~race~~
289
+ * ~~scroll~~
290
+ * ~~setEnsureElementIsInTheViewport~~
291
+ * ~~setTimeout~~
292
+ * ~~setVisibility~~
293
+ * ~~setWaitForEnabled~~
294
+ * ~~setWaitForStableBoundingBox~~
295
+ * ~~wait~~
296
+ * ~~waitHandle~~
297
+
264
298
  ## Mouse
265
299
 
266
300
  * click
@@ -271,6 +305,7 @@
271
305
  * dragOver => `#drag_over`
272
306
  * drop
273
307
  * move
308
+ * reset
274
309
  * up
275
310
  * wheel
276
311
 
@@ -280,11 +315,8 @@
280
315
  * $$ => `#query_selector_all`
281
316
  * $$eval => `#eval_on_selector_all`
282
317
  * $eval => `#eval_on_selector`
283
- * $x => `#Sx`
284
318
  * addScriptTag => `#add_script_tag`
285
319
  * addStyleTag => `#add_style_tag`
286
- * addStyleTag => `#add_style_tag`
287
- * addStyleTag => `#add_style_tag`
288
320
  * authenticate
289
321
  * bringToFront => `#bring_to_front`
290
322
  * browser
@@ -293,10 +325,12 @@
293
325
  * close
294
326
  * content
295
327
  * cookies
328
+ * ~~createCDPSession~~
296
329
  * createPDFStream => `#create_pdf_stream`
297
330
  * deleteCookie => `#delete_cookie`
298
331
  * emulate
299
332
  * emulateCPUThrottling => `#emulate_cpu_throttling`
333
+ * ~~emulateFocusedPage~~
300
334
  * emulateIdleState => `#emulate_idle_state`
301
335
  * emulateMediaFeatures => `#emulate_media_features`
302
336
  * emulateMediaType => `#emulate_media_type`
@@ -309,6 +343,7 @@
309
343
  * exposeFunction => `#expose_function`
310
344
  * focus
311
345
  * frames
346
+ * ~~getDefaultNavigationTimeout~~
312
347
  * ~~getDefaultTimeout~~
313
348
  * goBack => `#go_back`
314
349
  * goForward => `#go_forward`
@@ -317,17 +352,22 @@
317
352
  * isClosed => `#closed?`
318
353
  * isDragInterceptionEnabled => `#drag_interception_enabled?`
319
354
  * isJavaScriptEnabled => `#javascript_enabled?`
355
+ * isServiceWorkerBypassed => `#service_worker_bypassed?`
356
+ * ~~locator~~
320
357
  * mainFrame => `#main_frame`
321
358
  * metrics
322
- * ~~off~~
323
- * on
324
- * once
359
+ * ~~openDevTools~~
325
360
  * pdf
326
361
  * queryObjects => `#query_objects`
327
362
  * reload
363
+ * removeExposedFunction => `#remove_exposed_function`
364
+ * removeScriptToEvaluateOnNewDocument => `#remove_script_to_evaluate_on_new_document`
365
+ * ~~resize~~
366
+ * ~~screencast~~
328
367
  * screenshot
329
368
  * select
330
369
  * setBypassCSP => `#bypass_csp=`
370
+ * ~~setBypassServiceWorker~~
331
371
  * setCacheEnabled => `#cache_enabled=`
332
372
  * setContent => `#content=`
333
373
  * setCookie => `#set_cookie`
@@ -347,26 +387,24 @@
347
387
  * type => `#type_text`
348
388
  * url
349
389
  * viewport
390
+ * ~~waitForDevicePrompt~~
350
391
  * waitForFileChooser => `#wait_for_file_chooser`
351
392
  * waitForFrame => `#wait_for_frame`
352
393
  * waitForFunction => `#wait_for_function`
353
394
  * waitForNavigation => `#wait_for_navigation`
354
- * ~~waitForNetworkIdle~~
395
+ * waitForNetworkIdle => `#wait_for_network_idle`
355
396
  * waitForRequest => `#wait_for_request`
356
397
  * waitForResponse => `#wait_for_response`
357
398
  * waitForSelector => `#wait_for_selector`
358
- * waitForTimeout => `#wait_for_timeout`
359
- * waitForXPath => `#wait_for_xpath`
399
+ * ~~windowId~~
360
400
  * workers
361
401
 
362
- ## ~~ProductLauncher~~
402
+ ## ~~ProtocolError~~
363
403
 
364
- * ~~defaultArgs~~
365
- * ~~executablePath~~
366
- * ~~launch~~
367
404
 
368
- ## ~~ProtocolError~~
405
+ ## ~~ScreenRecorder~~
369
406
 
407
+ * ~~stop~~
370
408
 
371
409
  ## ~~SecurityDetails~~
372
410
 
@@ -379,6 +417,7 @@
379
417
 
380
418
  ## Target
381
419
 
420
+ * ~~asPage~~
382
421
  * browser
383
422
  * browserContext => `#browser_context`
384
423
  * createCDPSession => `#create_cdp_session`
@@ -386,22 +425,32 @@
386
425
  * page
387
426
  * type
388
427
  * url
389
- * ~~worker~~
428
+ * worker
390
429
 
391
430
  ## TimeoutError
392
431
 
393
432
 
394
- ## ~~Touchscreen~~
433
+ ## TouchError
395
434
 
396
- * ~~tap~~
435
+
436
+ ## Touchscreen
437
+
438
+ * tap
439
+ * touchEnd => `#touch_end`
440
+ * touchMove => `#touch_move`
441
+ * touchStart => `#touch_start`
397
442
 
398
443
  ## Tracing
399
444
 
400
445
  * start
401
446
  * stop
402
447
 
403
- ## ~~WebWorker~~
448
+ ## ~~UnsupportedOperation~~
449
+
404
450
 
405
- * ~~evaluate~~
406
- * ~~evaluateHandle~~
407
- * ~~url~~
451
+ ## WebWorker
452
+
453
+ * close
454
+ * evaluate
455
+ * evaluateHandle => `#evaluate_handle`
456
+ * url
@@ -48,7 +48,7 @@ class Puppeteer::AriaQueryHandler
48
48
  end
49
49
  end
50
50
 
51
- def wait_for(element_or_frame, selector, visible: nil, hidden: nil, timeout: nil)
51
+ def wait_for(element_or_frame, selector, visible: nil, hidden: nil, timeout: nil, polling: nil)
52
52
  case element_or_frame
53
53
  when Puppeteer::Frame
54
54
  frame = element_or_frame
@@ -80,6 +80,7 @@ class Puppeteer::AriaQueryHandler
80
80
  visible: visible,
81
81
  hidden: hidden,
82
82
  timeout: timeout,
83
+ polling: polling,
83
84
  binding_function: binding_function,
84
85
  )
85
86
 
@@ -103,7 +104,7 @@ class Puppeteer::AriaQueryHandler
103
104
  promises = res.map do |ax_node|
104
105
  world.send(:async_adopt_backend_node, ax_node['backendDOMNodeId'])
105
106
  end
106
- await_all(*promises)
107
+ Puppeteer::AsyncUtils.await_promise_all(*promises)
107
108
  end
108
109
  end
109
110
 
@@ -0,0 +1,214 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "async"
4
+ require "async/barrier"
5
+ require "async/promise"
6
+
7
+ module Puppeteer
8
+ # Async helpers for Promise-style coordination using socketry/async.
9
+ module AsyncUtils
10
+ extend self
11
+
12
+ def await(task)
13
+ if task.is_a?(Proc)
14
+ task.call
15
+ elsif task.is_a?(Async::Promise)
16
+ current_task = Async::Task.current?
17
+ if current_task
18
+ until task.resolved?
19
+ current_task.sleep(0.001)
20
+ end
21
+ end
22
+ task.wait
23
+ elsif task.respond_to?(:wait)
24
+ task.wait
25
+ else
26
+ task
27
+ end
28
+ end
29
+
30
+ # Execute a task with a timeout using Async::Task#with_timeout.
31
+ # @param timeout_ms [Numeric] Timeout duration in milliseconds (0 means no timeout)
32
+ # @param task [Proc, Async::Promise, nil] Task to execute; falls back to block
33
+ # @yield [async_task] Execute a task within the timeout, optionally receiving Async::Task
34
+ # @return [Async::Task] Async task that resolves/rejects once the operation completes
35
+ def async_timeout(timeout_ms, task = nil, &block)
36
+ if task
37
+ runner = lambda do |async_task|
38
+ if timeout_ms == 0
39
+ if task.is_a?(Proc)
40
+ args = task.arity.positive? ? [async_task] : []
41
+ task.call(*args)
42
+ else
43
+ await(task)
44
+ end
45
+ else
46
+ timeout_seconds = timeout_ms / 1000.0
47
+ async_task.with_timeout(timeout_seconds) do
48
+ if task.is_a?(Proc)
49
+ args = task.arity.positive? ? [async_task] : []
50
+ task.call(*args)
51
+ else
52
+ await(task)
53
+ end
54
+ end
55
+ end
56
+ end
57
+ elsif block
58
+ runner = lambda do |async_task|
59
+ if timeout_ms == 0
60
+ args = block.arity.positive? ? [async_task] : []
61
+ await(block.call(*args))
62
+ else
63
+ timeout_seconds = timeout_ms / 1000.0
64
+ async_task.with_timeout(timeout_seconds) do
65
+ args = block.arity.positive? ? [async_task] : []
66
+ await(block.call(*args))
67
+ end
68
+ end
69
+ end
70
+ else
71
+ raise ArgumentError.new("AsyncUtils.async_timeout requires a task or block")
72
+ end
73
+
74
+ current_task = Async::Task.current?
75
+ if current_task
76
+ return current_task.async do |async_task|
77
+ runner.call(async_task)
78
+ end
79
+ end
80
+
81
+ result = nil
82
+ error = nil
83
+ Async do |async_task|
84
+ result = runner.call(async_task)
85
+ rescue => err
86
+ error = err
87
+ end.wait
88
+
89
+ ImmediateTask.new(result, error)
90
+ end
91
+
92
+ # Wait for all async tasks to complete and return results.
93
+ def await_promise_all(*tasks)
94
+ Sync { zip(*tasks) }
95
+ end
96
+
97
+ # Race multiple async tasks and return the result of the first one to complete.
98
+ def await_promise_race(*tasks)
99
+ Sync { first(*tasks) }
100
+ end
101
+
102
+ def future_with_logging(&block)
103
+ proc do |*block_args|
104
+ block.call(*block_args)
105
+ rescue ::Puppeteer::TimeoutError
106
+ raise
107
+ rescue => err
108
+ warn("#{err.message} (#{err.class})")
109
+ raise err
110
+ end
111
+ end
112
+
113
+ def sleep_seconds(duration)
114
+ task = Async::Task.current
115
+ if task
116
+ task.sleep(duration)
117
+ else
118
+ Kernel.sleep(duration)
119
+ end
120
+ rescue RuntimeError, NoMethodError
121
+ Kernel.sleep(duration)
122
+ end
123
+
124
+ class ImmediateTask
125
+ def initialize(result, error)
126
+ @result = result
127
+ @error = error
128
+ end
129
+
130
+ def wait
131
+ raise @error if @error
132
+
133
+ @result
134
+ end
135
+ end
136
+
137
+ private def zip(*tasks)
138
+ barrier = Async::Barrier.new
139
+ results = Array.new(tasks.size)
140
+
141
+ begin
142
+ tasks.each_with_index do |task, index|
143
+ barrier.async do
144
+ results[index] = await(task)
145
+ end
146
+ end
147
+
148
+ barrier.wait
149
+ results
150
+ rescue Exception
151
+ drain_barrier_tasks(barrier)
152
+ raise
153
+ end
154
+ end
155
+
156
+ private def first(*tasks)
157
+ barrier = Async::Barrier.new
158
+ result = nil
159
+
160
+ begin
161
+ tasks.each do |task|
162
+ barrier.async(finished: false) do
163
+ await(task)
164
+ end
165
+ end
166
+
167
+ barrier.wait do |completed_task|
168
+ result = completed_task.wait
169
+ break
170
+ end
171
+
172
+ result
173
+ ensure
174
+ drain_barrier_tasks(barrier)
175
+ end
176
+ end
177
+
178
+ private def drain_barrier_tasks(barrier)
179
+ pending = barrier.tasks.to_a
180
+ return if pending.empty?
181
+
182
+ barrier.stop
183
+ pending.each do |waiting|
184
+ task = waiting.task
185
+ next unless task.completed? || task.failed? || task.stopped?
186
+
187
+ begin
188
+ task.wait
189
+ rescue StandardError
190
+ # The race winner is already decided; ignore losers' errors.
191
+ end
192
+ end
193
+ end
194
+ end
195
+ end
196
+
197
+ module AsyncPromiseWaitRetry
198
+ def wait(...)
199
+ loop do
200
+ begin
201
+ return super
202
+ rescue ThreadError => e
203
+ raise unless e.message == 'Attempt to unlock a mutex which is not locked'
204
+ next unless resolved?
205
+
206
+ value = self.value
207
+ raise value if value.is_a?(Exception)
208
+ return value
209
+ end
210
+ end
211
+ end
212
+ end
213
+
214
+ Async::Promise.prepend(AsyncPromiseWaitRetry)