puppeteer-bidi 0.0.1.beta1
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 +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +13 -0
- data/CLAUDE/README.md +158 -0
- data/CLAUDE/async_programming.md +158 -0
- data/CLAUDE/click_implementation.md +340 -0
- data/CLAUDE/core_layer_gotchas.md +136 -0
- data/CLAUDE/error_handling.md +232 -0
- data/CLAUDE/file_chooser.md +95 -0
- data/CLAUDE/frame_architecture.md +346 -0
- data/CLAUDE/javascript_evaluation.md +341 -0
- data/CLAUDE/jshandle_implementation.md +505 -0
- data/CLAUDE/keyboard_implementation.md +250 -0
- data/CLAUDE/mouse_implementation.md +140 -0
- data/CLAUDE/navigation_waiting.md +234 -0
- data/CLAUDE/porting_puppeteer.md +214 -0
- data/CLAUDE/query_handler.md +194 -0
- data/CLAUDE/rspec_pending_vs_skip.md +262 -0
- data/CLAUDE/selector_evaluation.md +198 -0
- data/CLAUDE/test_server_routes.md +263 -0
- data/CLAUDE/testing_strategy.md +236 -0
- data/CLAUDE/two_layer_architecture.md +180 -0
- data/CLAUDE/wrapped_element_click.md +247 -0
- data/CLAUDE.md +185 -0
- data/LICENSE.txt +21 -0
- data/README.md +488 -0
- data/Rakefile +21 -0
- data/lib/puppeteer/bidi/async_utils.rb +151 -0
- data/lib/puppeteer/bidi/browser.rb +285 -0
- data/lib/puppeteer/bidi/browser_context.rb +53 -0
- data/lib/puppeteer/bidi/browser_launcher.rb +240 -0
- data/lib/puppeteer/bidi/connection.rb +182 -0
- data/lib/puppeteer/bidi/core/README.md +169 -0
- data/lib/puppeteer/bidi/core/browser.rb +230 -0
- data/lib/puppeteer/bidi/core/browsing_context.rb +601 -0
- data/lib/puppeteer/bidi/core/disposable.rb +69 -0
- data/lib/puppeteer/bidi/core/errors.rb +64 -0
- data/lib/puppeteer/bidi/core/event_emitter.rb +83 -0
- data/lib/puppeteer/bidi/core/navigation.rb +128 -0
- data/lib/puppeteer/bidi/core/realm.rb +315 -0
- data/lib/puppeteer/bidi/core/request.rb +300 -0
- data/lib/puppeteer/bidi/core/session.rb +153 -0
- data/lib/puppeteer/bidi/core/user_context.rb +208 -0
- data/lib/puppeteer/bidi/core/user_prompt.rb +102 -0
- data/lib/puppeteer/bidi/core.rb +45 -0
- data/lib/puppeteer/bidi/deserializer.rb +132 -0
- data/lib/puppeteer/bidi/element_handle.rb +602 -0
- data/lib/puppeteer/bidi/errors.rb +42 -0
- data/lib/puppeteer/bidi/file_chooser.rb +52 -0
- data/lib/puppeteer/bidi/frame.rb +597 -0
- data/lib/puppeteer/bidi/http_response.rb +23 -0
- data/lib/puppeteer/bidi/injected.js +1 -0
- data/lib/puppeteer/bidi/injected_source.rb +21 -0
- data/lib/puppeteer/bidi/js_handle.rb +302 -0
- data/lib/puppeteer/bidi/keyboard.rb +265 -0
- data/lib/puppeteer/bidi/lazy_arg.rb +23 -0
- data/lib/puppeteer/bidi/mouse.rb +170 -0
- data/lib/puppeteer/bidi/page.rb +613 -0
- data/lib/puppeteer/bidi/query_handler.rb +397 -0
- data/lib/puppeteer/bidi/realm.rb +242 -0
- data/lib/puppeteer/bidi/serializer.rb +139 -0
- data/lib/puppeteer/bidi/target.rb +81 -0
- data/lib/puppeteer/bidi/task_manager.rb +44 -0
- data/lib/puppeteer/bidi/timeout_settings.rb +20 -0
- data/lib/puppeteer/bidi/transport.rb +129 -0
- data/lib/puppeteer/bidi/version.rb +7 -0
- data/lib/puppeteer/bidi/wait_task.rb +322 -0
- data/lib/puppeteer/bidi.rb +49 -0
- data/scripts/update_injected_source.rb +57 -0
- data/sig/puppeteer/bidi/browser.rbs +80 -0
- data/sig/puppeteer/bidi/element_handle.rbs +238 -0
- data/sig/puppeteer/bidi/frame.rbs +205 -0
- data/sig/puppeteer/bidi/js_handle.rbs +90 -0
- data/sig/puppeteer/bidi/page.rbs +247 -0
- data/sig/puppeteer/bidi.rbs +15 -0
- metadata +176 -0
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Puppeteer
|
|
4
|
+
module Bidi
|
|
5
|
+
module Core
|
|
6
|
+
# Base error class for Core module
|
|
7
|
+
class Error < Puppeteer::Bidi::Error
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
# Raised when attempting to use a disposed resource
|
|
11
|
+
class DisposedError < Error
|
|
12
|
+
attr_reader :resource_type, :reason
|
|
13
|
+
|
|
14
|
+
def initialize(resource_type, reason)
|
|
15
|
+
@resource_type = resource_type
|
|
16
|
+
@reason = reason
|
|
17
|
+
super("#{resource_type} already disposed: #{reason}")
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Raised when a realm has been destroyed
|
|
22
|
+
class RealmDestroyedError < DisposedError
|
|
23
|
+
def initialize(reason)
|
|
24
|
+
super('Realm', reason)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Raised when a browsing context has been closed
|
|
29
|
+
class BrowsingContextClosedError < DisposedError
|
|
30
|
+
def initialize(reason)
|
|
31
|
+
super('Browsing context', reason)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Raised when a user context has been closed
|
|
36
|
+
class UserContextClosedError < DisposedError
|
|
37
|
+
def initialize(reason)
|
|
38
|
+
super('User context', reason)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Raised when a user prompt has been closed
|
|
43
|
+
class UserPromptClosedError < DisposedError
|
|
44
|
+
def initialize(reason)
|
|
45
|
+
super('User prompt', reason)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Raised when a session has ended
|
|
50
|
+
class SessionEndedError < DisposedError
|
|
51
|
+
def initialize(reason)
|
|
52
|
+
super('Session', reason)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Raised when a browser has been disconnected
|
|
57
|
+
class BrowserDisconnectedError < DisposedError
|
|
58
|
+
def initialize(reason)
|
|
59
|
+
super('Browser', reason)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Puppeteer
|
|
4
|
+
module Bidi
|
|
5
|
+
module Core
|
|
6
|
+
# EventEmitter provides event subscription and emission capabilities
|
|
7
|
+
# Similar to Node.js EventEmitter but simpler
|
|
8
|
+
class EventEmitter
|
|
9
|
+
def initialize
|
|
10
|
+
@listeners = Hash.new { |h, k| h[k] = [] }
|
|
11
|
+
@disposed = false
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Register an event listener
|
|
15
|
+
# @param event [Symbol, String] Event name
|
|
16
|
+
# @param block [Proc] Event handler
|
|
17
|
+
def on(event, &block)
|
|
18
|
+
return if @disposed
|
|
19
|
+
@listeners[event.to_sym] << block
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Register a one-time event listener
|
|
23
|
+
# @param event [Symbol, String] Event name
|
|
24
|
+
# @param block [Proc] Event handler
|
|
25
|
+
def once(event, &block)
|
|
26
|
+
return if @disposed
|
|
27
|
+
wrapper = lambda do |*args|
|
|
28
|
+
off(event, &wrapper)
|
|
29
|
+
block.call(*args)
|
|
30
|
+
end
|
|
31
|
+
on(event, &wrapper)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Remove an event listener
|
|
35
|
+
# @param event [Symbol, String] Event name
|
|
36
|
+
# @param block [Proc] Event handler to remove (optional)
|
|
37
|
+
def off(event, &block)
|
|
38
|
+
event_sym = event.to_sym
|
|
39
|
+
if block
|
|
40
|
+
@listeners[event_sym].delete(block)
|
|
41
|
+
else
|
|
42
|
+
@listeners.delete(event_sym)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Remove all listeners for an event or all events
|
|
47
|
+
# @param event [Symbol, String, nil] Event name (optional)
|
|
48
|
+
def remove_all_listeners(event = nil)
|
|
49
|
+
if event
|
|
50
|
+
@listeners.delete(event.to_sym)
|
|
51
|
+
else
|
|
52
|
+
@listeners.clear
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Emit an event to all registered listeners
|
|
57
|
+
# @param event [Symbol, String] Event name
|
|
58
|
+
# @param data [Object] Event data
|
|
59
|
+
def emit(event, data = nil)
|
|
60
|
+
return if @disposed
|
|
61
|
+
listeners = @listeners[event.to_sym].dup
|
|
62
|
+
listeners.each do |listener|
|
|
63
|
+
begin
|
|
64
|
+
listener.call(data)
|
|
65
|
+
rescue => e
|
|
66
|
+
warn "Error in event listener for #{event}: #{e.message}\n#{e.backtrace.join("\n")}"
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Dispose the event emitter
|
|
72
|
+
def dispose
|
|
73
|
+
@disposed = true
|
|
74
|
+
@listeners.clear
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def disposed?
|
|
78
|
+
@disposed
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Puppeteer
|
|
4
|
+
module Bidi
|
|
5
|
+
module Core
|
|
6
|
+
# Navigation represents a single navigation operation
|
|
7
|
+
class Navigation < EventEmitter
|
|
8
|
+
include Disposable::DisposableMixin
|
|
9
|
+
|
|
10
|
+
# Create a navigation instance
|
|
11
|
+
# @param browsing_context [BrowsingContext] The browsing context
|
|
12
|
+
# @return [Navigation] New navigation instance
|
|
13
|
+
def self.from(browsing_context)
|
|
14
|
+
navigation = new(browsing_context)
|
|
15
|
+
navigation.send(:initialize_navigation)
|
|
16
|
+
navigation
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
attr_reader :browsing_context, :request
|
|
20
|
+
|
|
21
|
+
def initialize(browsing_context)
|
|
22
|
+
super()
|
|
23
|
+
@browsing_context = browsing_context
|
|
24
|
+
@request = nil
|
|
25
|
+
@navigation = nil
|
|
26
|
+
@id = nil
|
|
27
|
+
@disposables = Disposable::DisposableStack.new
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Get the nested navigation if any
|
|
31
|
+
# @return [Navigation, nil] Nested navigation
|
|
32
|
+
def navigation
|
|
33
|
+
@navigation
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
protected
|
|
37
|
+
|
|
38
|
+
def perform_dispose
|
|
39
|
+
@disposables.dispose
|
|
40
|
+
super
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def session
|
|
46
|
+
@browsing_context.user_context.browser.session
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def initialize_navigation
|
|
50
|
+
# Listen for browsing context closure
|
|
51
|
+
@browsing_context.once(:closed) do
|
|
52
|
+
emit(:failed, {
|
|
53
|
+
url: @browsing_context.url,
|
|
54
|
+
timestamp: Time.now
|
|
55
|
+
})
|
|
56
|
+
dispose
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Listen for requests with navigation ID
|
|
60
|
+
@browsing_context.on(:request) do |data|
|
|
61
|
+
request = data[:request]
|
|
62
|
+
next unless request.navigation
|
|
63
|
+
next unless matches?(request.navigation)
|
|
64
|
+
|
|
65
|
+
@request = request
|
|
66
|
+
emit(:request, request)
|
|
67
|
+
|
|
68
|
+
# Listen for redirects
|
|
69
|
+
request.on(:redirect) do |redirect_request|
|
|
70
|
+
@request = redirect_request
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Listen for nested navigation
|
|
75
|
+
session.on('browsingContext.navigationStarted') do |info|
|
|
76
|
+
next unless info['context'] == @browsing_context.id
|
|
77
|
+
next if @navigation && !@navigation.disposed?
|
|
78
|
+
|
|
79
|
+
@navigation = Navigation.from(@browsing_context)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Listen for navigation completion events
|
|
83
|
+
%w[browsingContext.domContentLoaded browsingContext.load].each do |event_name|
|
|
84
|
+
session.on(event_name) do |info|
|
|
85
|
+
next unless info['context'] == @browsing_context.id
|
|
86
|
+
next if info['navigation'].nil?
|
|
87
|
+
next unless matches?(info['navigation'])
|
|
88
|
+
|
|
89
|
+
dispose
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Listen for navigation end events
|
|
94
|
+
{
|
|
95
|
+
'browsingContext.fragmentNavigated' => :fragment,
|
|
96
|
+
'browsingContext.navigationFailed' => :failed,
|
|
97
|
+
'browsingContext.navigationAborted' => :aborted
|
|
98
|
+
}.each do |event_name, emit_event|
|
|
99
|
+
session.on(event_name) do |info|
|
|
100
|
+
next unless info['context'] == @browsing_context.id
|
|
101
|
+
next unless matches?(info['navigation'])
|
|
102
|
+
|
|
103
|
+
emit(emit_event, {
|
|
104
|
+
url: info['url'],
|
|
105
|
+
timestamp: Time.at(info['timestamp'] / 1000.0)
|
|
106
|
+
})
|
|
107
|
+
dispose
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def matches?(navigation_id)
|
|
113
|
+
# If nested navigation exists and is not disposed, this navigation doesn't match
|
|
114
|
+
return false if @navigation && !@navigation.disposed?
|
|
115
|
+
|
|
116
|
+
# First navigation event sets the ID
|
|
117
|
+
if @id.nil?
|
|
118
|
+
@id = navigation_id
|
|
119
|
+
return true
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Check if the navigation ID matches
|
|
123
|
+
@id == navigation_id
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Puppeteer
|
|
4
|
+
module Bidi
|
|
5
|
+
module Core
|
|
6
|
+
# Realm is the base class for script execution realms
|
|
7
|
+
class Realm < EventEmitter
|
|
8
|
+
include Disposable::DisposableMixin
|
|
9
|
+
|
|
10
|
+
attr_reader :id, :origin
|
|
11
|
+
|
|
12
|
+
def initialize(id, origin)
|
|
13
|
+
super()
|
|
14
|
+
@id = id
|
|
15
|
+
@origin = origin
|
|
16
|
+
@reason = nil
|
|
17
|
+
@execution_context_id = nil
|
|
18
|
+
@disposables = Disposable::DisposableStack.new
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Get the target for script execution
|
|
22
|
+
# @return [Hash] BiDi target descriptor
|
|
23
|
+
def target
|
|
24
|
+
{ realm: @id }
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Disown handles (remove references)
|
|
28
|
+
# @param handles [Array<String>] Handle IDs to disown
|
|
29
|
+
def disown(handles)
|
|
30
|
+
raise RealmDestroyedError, @reason if disposed?
|
|
31
|
+
session.async_send_command('script.disown', {
|
|
32
|
+
target: target,
|
|
33
|
+
handles: handles
|
|
34
|
+
})
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Call a function in the realm
|
|
38
|
+
# @param function_declaration [String] Function source code
|
|
39
|
+
# @param await_promise [Boolean] Whether to await promise results (Note: different from returnByValue!)
|
|
40
|
+
# @param options [Hash] Additional options (arguments, serializationOptions, resultOwnership, etc.)
|
|
41
|
+
# @return [Hash] Evaluation result (with 'type', 'realm', and optionally 'result'/'exceptionDetails')
|
|
42
|
+
def call_function(function_declaration, await_promise, **options)
|
|
43
|
+
raise RealmDestroyedError, @reason if disposed?
|
|
44
|
+
|
|
45
|
+
# Note: In Puppeteer, returnByValue controls serialization, not awaitPromise
|
|
46
|
+
# awaitPromise controls whether to wait for promises
|
|
47
|
+
# For BiDi, we use 'root' ownership by default to keep handles alive
|
|
48
|
+
# Only use 'none' if explicitly requested
|
|
49
|
+
params = {
|
|
50
|
+
functionDeclaration: function_declaration,
|
|
51
|
+
awaitPromise: await_promise,
|
|
52
|
+
target: target,
|
|
53
|
+
resultOwnership: 'root',
|
|
54
|
+
**options
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
session.async_send_command('script.callFunction', params)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Evaluate an expression in the realm
|
|
61
|
+
# @param expression [String] JavaScript expression
|
|
62
|
+
# @param await_promise [Boolean] Whether to await promise results
|
|
63
|
+
# @param options [Hash] Additional options (serializationOptions, resultOwnership, etc.)
|
|
64
|
+
# @return [Hash] Evaluation result (with 'type', 'realm', and optionally 'result'/'exceptionDetails')
|
|
65
|
+
def evaluate(expression, await_promise, **options)
|
|
66
|
+
raise RealmDestroyedError, @reason if disposed?
|
|
67
|
+
|
|
68
|
+
# Use 'root' ownership by default to keep handles alive
|
|
69
|
+
params = {
|
|
70
|
+
expression: expression,
|
|
71
|
+
awaitPromise: await_promise,
|
|
72
|
+
target: target,
|
|
73
|
+
resultOwnership: 'root',
|
|
74
|
+
**options
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
session.async_send_command('script.evaluate', params)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Resolve the CDP execution context ID for this realm
|
|
81
|
+
# @return [Integer] Execution context ID
|
|
82
|
+
def resolve_execution_context_id
|
|
83
|
+
return @execution_context_id if @execution_context_id
|
|
84
|
+
|
|
85
|
+
# This uses a Chrome-specific extension to BiDi
|
|
86
|
+
result = session.connection.send_command('goog:cdp.resolveRealm', { realm: @id })
|
|
87
|
+
@execution_context_id = result['executionContextId']
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
protected
|
|
91
|
+
|
|
92
|
+
# Abstract method - must be implemented by subclasses
|
|
93
|
+
# @return [Session] The session for this realm
|
|
94
|
+
def session
|
|
95
|
+
raise NotImplementedError, 'Subclasses must implement #session'
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def perform_dispose
|
|
99
|
+
@reason ||= 'Realm destroyed, probably because all associated browsing contexts closed'
|
|
100
|
+
emit(:destroyed, { reason: @reason })
|
|
101
|
+
@disposables.dispose
|
|
102
|
+
super
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# WindowRealm represents a JavaScript realm in a window or iframe
|
|
107
|
+
class WindowRealm < Realm
|
|
108
|
+
# Create a window realm
|
|
109
|
+
# @param browsing_context [BrowsingContext] The browsing context
|
|
110
|
+
# @param sandbox [String, nil] Sandbox name
|
|
111
|
+
# @return [WindowRealm] New window realm
|
|
112
|
+
def self.from(browsing_context, sandbox = nil)
|
|
113
|
+
realm = new(browsing_context, sandbox)
|
|
114
|
+
realm.send(:initialize_realm)
|
|
115
|
+
realm
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
attr_reader :browsing_context, :sandbox
|
|
119
|
+
|
|
120
|
+
def initialize(browsing_context, sandbox = nil)
|
|
121
|
+
super('', '') # ID and origin will be set when realm is created
|
|
122
|
+
@browsing_context = browsing_context
|
|
123
|
+
@sandbox = sandbox
|
|
124
|
+
@workers = {}
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Set the environment (Frame) for this realm
|
|
128
|
+
# This is set by Frame when it's created
|
|
129
|
+
# @param frame [Frame] The frame environment
|
|
130
|
+
def environment=(frame)
|
|
131
|
+
@environment = frame
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Get the environment (Frame) for this realm
|
|
135
|
+
# @return [Frame] The frame environment
|
|
136
|
+
def environment
|
|
137
|
+
@environment
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Override target to use context-based target
|
|
141
|
+
# @return [Hash] BiDi target descriptor
|
|
142
|
+
def target
|
|
143
|
+
result = { context: @browsing_context.id }
|
|
144
|
+
result[:sandbox] = @sandbox if @sandbox
|
|
145
|
+
result
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
protected
|
|
149
|
+
|
|
150
|
+
def session
|
|
151
|
+
@browsing_context.user_context.browser.session
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
private
|
|
155
|
+
|
|
156
|
+
def initialize_realm
|
|
157
|
+
# Listen for browsing context closure
|
|
158
|
+
@browsing_context.once(:closed) do |data|
|
|
159
|
+
dispose_realm(data[:reason])
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# Listen for realm creation (this realm)
|
|
163
|
+
session.on('script.realmCreated') do |info|
|
|
164
|
+
next unless info['type'] == 'window'
|
|
165
|
+
next unless info['context'] == @browsing_context.id
|
|
166
|
+
next unless info['sandbox'] == @sandbox
|
|
167
|
+
|
|
168
|
+
# Set the ID and origin for this realm
|
|
169
|
+
@id = info['realm']
|
|
170
|
+
@origin = info['origin']
|
|
171
|
+
@execution_context_id = nil
|
|
172
|
+
emit(:updated, self)
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# Listen for dedicated worker creation
|
|
176
|
+
session.on('script.realmCreated') do |info|
|
|
177
|
+
next unless info['type'] == 'dedicated-worker'
|
|
178
|
+
next unless info['owners']&.include?(@id)
|
|
179
|
+
|
|
180
|
+
worker = DedicatedWorkerRealm.from(self, info['realm'], info['origin'])
|
|
181
|
+
@workers[worker.id] = worker
|
|
182
|
+
|
|
183
|
+
worker.once(:destroyed) do
|
|
184
|
+
@workers.delete(worker.id)
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
emit(:worker, worker)
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def dispose_realm(reason)
|
|
192
|
+
@reason = reason
|
|
193
|
+
dispose
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# DedicatedWorkerRealm represents a JavaScript realm in a dedicated worker
|
|
198
|
+
class DedicatedWorkerRealm < Realm
|
|
199
|
+
# Create a dedicated worker realm
|
|
200
|
+
# @param owner [WindowRealm, DedicatedWorkerRealm, SharedWorkerRealm] Owner realm
|
|
201
|
+
# @param id [String] Realm ID
|
|
202
|
+
# @param origin [String] Realm origin
|
|
203
|
+
# @return [DedicatedWorkerRealm] New dedicated worker realm
|
|
204
|
+
def self.from(owner, id, origin)
|
|
205
|
+
realm = new(owner, id, origin)
|
|
206
|
+
realm.send(:initialize_realm)
|
|
207
|
+
realm
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
attr_reader :owners
|
|
211
|
+
|
|
212
|
+
def initialize(owner, id, origin)
|
|
213
|
+
super(id, origin)
|
|
214
|
+
@owners = Set.new([owner])
|
|
215
|
+
@workers = {}
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
protected
|
|
219
|
+
|
|
220
|
+
def session
|
|
221
|
+
# Get session from any owner
|
|
222
|
+
@owners.first.session
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
private
|
|
226
|
+
|
|
227
|
+
def initialize_realm
|
|
228
|
+
# Listen for realm destruction
|
|
229
|
+
session.on('script.realmDestroyed') do |info|
|
|
230
|
+
next unless info['realm'] == @id
|
|
231
|
+
dispose_realm('Realm destroyed')
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
# Listen for nested dedicated worker creation
|
|
235
|
+
session.on('script.realmCreated') do |info|
|
|
236
|
+
next unless info['type'] == 'dedicated-worker'
|
|
237
|
+
next unless info['owners']&.include?(@id)
|
|
238
|
+
|
|
239
|
+
worker = DedicatedWorkerRealm.from(self, info['realm'], info['origin'])
|
|
240
|
+
@workers[worker.id] = worker
|
|
241
|
+
|
|
242
|
+
worker.once(:destroyed) do
|
|
243
|
+
@workers.delete(worker.id)
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
emit(:worker, worker)
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
def dispose_realm(reason)
|
|
251
|
+
@reason = reason
|
|
252
|
+
dispose
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
# SharedWorkerRealm represents a JavaScript realm in a shared worker
|
|
257
|
+
class SharedWorkerRealm < Realm
|
|
258
|
+
# Create a shared worker realm
|
|
259
|
+
# @param browser [Browser] Browser instance
|
|
260
|
+
# @param id [String] Realm ID
|
|
261
|
+
# @param origin [String] Realm origin
|
|
262
|
+
# @return [SharedWorkerRealm] New shared worker realm
|
|
263
|
+
def self.from(browser, id, origin)
|
|
264
|
+
realm = new(browser, id, origin)
|
|
265
|
+
realm.send(:initialize_realm)
|
|
266
|
+
realm
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
attr_reader :browser
|
|
270
|
+
|
|
271
|
+
def initialize(browser, id, origin)
|
|
272
|
+
super(id, origin)
|
|
273
|
+
@browser = browser
|
|
274
|
+
@workers = {}
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
protected
|
|
278
|
+
|
|
279
|
+
def session
|
|
280
|
+
@browser.session
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
private
|
|
284
|
+
|
|
285
|
+
def initialize_realm
|
|
286
|
+
# Listen for realm destruction
|
|
287
|
+
session.on('script.realmDestroyed') do |info|
|
|
288
|
+
next unless info['realm'] == @id
|
|
289
|
+
dispose_realm('Realm destroyed')
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
# Listen for dedicated worker creation
|
|
293
|
+
session.on('script.realmCreated') do |info|
|
|
294
|
+
next unless info['type'] == 'dedicated-worker'
|
|
295
|
+
next unless info['owners']&.include?(@id)
|
|
296
|
+
|
|
297
|
+
worker = DedicatedWorkerRealm.from(self, info['realm'], info['origin'])
|
|
298
|
+
@workers[worker.id] = worker
|
|
299
|
+
|
|
300
|
+
worker.once(:destroyed) do
|
|
301
|
+
@workers.delete(worker.id)
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
emit(:worker, worker)
|
|
305
|
+
end
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
def dispose_realm(reason)
|
|
309
|
+
@reason = reason
|
|
310
|
+
dispose
|
|
311
|
+
end
|
|
312
|
+
end
|
|
313
|
+
end
|
|
314
|
+
end
|
|
315
|
+
end
|