Module: Obxcura::Frame::Runtime

Included in:
Obxcura::Frame
Defined in:
lib/obxcura/frame/runtime.rb

Overview

Runtime: executing JavaScript in the frame's context. This is the side that reaches the Client and puts CDP commands on the wire; everything in DOM is built on top of #evaluate.

Ruby values cross into JS as real arguments (Runtime.callFunctionOn), not by being string-interpolated into source. Node-handle resolution, cyclic-node detection and a retry loop are deliberately left out.

Constant Summary collapse

EVALUATE_CHUNK =

Obscura won't send a single CDP message larger than ~500-700KB, so we pull large strings back in slices this size and stitch them together. Well under the ceiling, and each slice is fast over the websocket-driver transport.

Returns:

400_000

Instance Method Summary collapse

Instance Method Details

#call_on(object_id, function_declaration, args = [], timeout: nil) ⇒ Object?

Call a function declaration bound to a specific remote object (its this). Used by #evaluate_func (bound to globalThis) and by Node reads (bound to the node's handle).

Parameters:

  • object_id (String)

    the CDP objectId to bind as this.

  • function_declaration (String)

    a JS function declaration.

  • args (Array) (defaults to: [])

    values passed to the function.

  • timeout (Integer, nil) (defaults to: nil)

    override for the client's reply timeout, e.g. for a call that awaits a slow in-page POST.

Returns:

  • (Object, nil)

    the function's return value (JSON-decoded).

Raises:



64
65
66
67
68
69
70
71
72
73
74
75
76
# File 'lib/obxcura/frame/runtime.rb', line 64

def call_on(object_id, function_declaration, args = [], timeout: nil)
  handle_result(command(
    "Runtime.callFunctionOn",
    {
      functionDeclaration: function_declaration,
      objectId: object_id,
      arguments: args.map { |value| { value: value } },
      awaitPromise: true,
      returnByValue: true
    },
    timeout: timeout
  ))
end

#evaluate(expression, *args) ⇒ Object?

Evaluate a JS expression and return its value (awaits promises).

Extra args are passed to the page as real values, reachable in the expression as arguments[0], arguments[1], ... Without args it's a single Runtime.evaluate (the hot path); with args it goes through Runtime.callFunctionOn so nothing is interpolated into source.

Examples:

frame.evaluate("arguments[0] + arguments[1]", 2, 3)   # => 5

Parameters:

  • expression (String)

    the JS expression to evaluate.

  • args (Array)

    values passed to the page as arguments[...].

Returns:

  • (Object, nil)

    the evaluated value (JSON-decoded).

Raises:



33
34
35
36
37
# File 'lib/obxcura/frame/runtime.rb', line 33

def evaluate(expression, *args)
  return handle_result(evaluate_command(expression)) if args.empty?

  evaluate_func("function() { return (#{expression}); }", *args)
end

#evaluate_func(expression, *args, timeout: nil) ⇒ Object?

Call a JS function declaration with the given arguments, bound to the live global object. Unlike #evaluate, expression is the function itself.

Examples:

frame.evaluate_func("function(sel){ return document.querySelector(sel) }", "#id")

Parameters:

  • expression (String)

    a JS function declaration.

  • args (Array)

    values passed to the function.

  • timeout (Integer, nil) (defaults to: nil)

    override for the client's reply timeout.

Returns:

  • (Object, nil)

    the function's return value (JSON-decoded).

Raises:



49
50
51
# File 'lib/obxcura/frame/runtime.rb', line 49

def evaluate_func(expression, *args, timeout: nil)
  call_on(global_object_id, expression, args, timeout: timeout)
end

#read_string(js_expression) ⇒ String

Pull a possibly-large JS string back in EVALUATE_CHUNK-sized slices. The expression is snapshotted into a page global once, so it's evaluated a single time no matter how big the result is; we then slice that global.

Parameters:

  • js_expression (String)

    a JS expression producing a string.

Returns:

  • (String)

    the full string, reassembled from slices.



84
85
86
87
88
89
90
91
92
93
94
95
# File 'lib/obxcura/frame/runtime.rb', line 84

def read_string(js_expression)
  length = evaluate("(window.__obxcura_read = String(#{js_expression})).length").to_i
  return "" if length.zero?

  buffer = String.new(capacity: length)
  offset = 0
  while offset < length
    buffer << evaluate("window.__obxcura_read.slice(#{offset}, #{offset + EVALUATE_CHUNK})")
    offset += EVALUATE_CHUNK
  end
  buffer
end