bidi2pdf 0.1.14 → 0.1.16
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 +4 -4
- data/.rubocop.yml +20 -0
- data/CHANGELOG.md +69 -2
- data/README.md +258 -10
- data/docker/Dockerfile +4 -0
- data/docker/Dockerfile.slim +5 -1
- data/lib/bidi2pdf/bidi/browser_tab.rb +45 -4
- data/lib/bidi2pdf/bidi/buffered_web_socket_client.rb +178 -0
- data/lib/bidi2pdf/bidi/client.rb +12 -5
- data/lib/bidi2pdf/bidi/commands/base.rb +2 -0
- data/lib/bidi2pdf/bidi/network_event.rb +12 -3
- data/lib/bidi2pdf/bidi/network_events.rb +9 -1
- data/lib/bidi2pdf/bidi/session.rb +1 -1
- data/lib/bidi2pdf/chromedriver_manager.rb +10 -6
- data/lib/bidi2pdf/cli/json_output.rb +30 -0
- data/lib/bidi2pdf/cli.rb +559 -17
- data/lib/bidi2pdf/diagnose.rb +119 -0
- data/lib/bidi2pdf/error_codes.rb +59 -0
- data/lib/bidi2pdf/exit_codes.rb +44 -0
- data/lib/bidi2pdf/launcher.rb +23 -0
- data/lib/bidi2pdf/manifest.rb +65 -0
- data/lib/bidi2pdf/notifications/json_subscriber.rb +78 -0
- data/lib/bidi2pdf/notifications/logging_subscriber.rb +2 -0
- data/lib/bidi2pdf/pdf_inspection.rb +62 -0
- data/lib/bidi2pdf/recipe/loader.rb +44 -0
- data/lib/bidi2pdf/recipe/runner.rb +229 -0
- data/lib/bidi2pdf/recipe/schema_shape.rb +228 -0
- data/lib/bidi2pdf/recipe/validator.rb +138 -0
- data/lib/bidi2pdf/recipe.rb +83 -0
- data/lib/bidi2pdf/result.rb +67 -0
- data/lib/bidi2pdf/result_collector.rb +150 -0
- data/lib/bidi2pdf/schema.rb +382 -0
- data/lib/bidi2pdf/session_runner.rb +42 -0
- data/lib/bidi2pdf/session_warmer.rb +377 -0
- data/lib/bidi2pdf/test_helpers/testcontainers/chromedriver_test_helper.rb +4 -2
- data/lib/bidi2pdf/version.rb +1 -1
- data/lib/bidi2pdf.rb +114 -9
- data/sig/bidi2pdf/bidi/browser_tab.rbs +17 -0
- data/sig/bidi2pdf/bidi/buffered_web_socket_client.rbs +81 -0
- data/sig/bidi2pdf/bidi/client.rbs +10 -2
- data/sig/bidi2pdf/bidi/network_event.rbs +11 -1
- data/sig/bidi2pdf/bidi/session.rbs +1 -1
- data/sig/bidi2pdf/chromedriver_manager.rbs +4 -0
- data/sig/bidi2pdf/cli/json_output.rbs +19 -0
- data/sig/bidi2pdf/cli.rbs +112 -2
- data/sig/bidi2pdf/diagnose.rbs +30 -0
- data/sig/bidi2pdf/error_codes.rbs +21 -0
- data/sig/bidi2pdf/exit_codes.rbs +21 -0
- data/sig/bidi2pdf/launcher.rbs +9 -0
- data/sig/bidi2pdf/manifest.rbs +38 -0
- data/sig/bidi2pdf/notifications/json_subscriber.rbs +45 -0
- data/sig/bidi2pdf/pdf_inspection.rbs +33 -0
- data/sig/bidi2pdf/recipe/loader.rbs +20 -0
- data/sig/bidi2pdf/recipe/runner.rbs +92 -0
- data/sig/bidi2pdf/recipe/schema_shape.rbs +85 -0
- data/sig/bidi2pdf/recipe/validator.rbs +54 -0
- data/sig/bidi2pdf/recipe.rbs +72 -0
- data/sig/bidi2pdf/result.rbs +71 -0
- data/sig/bidi2pdf/result_collector.rbs +106 -0
- data/sig/bidi2pdf/schema.rbs +45 -0
- data/sig/bidi2pdf/session_runner.rbs +17 -0
- data/sig/bidi2pdf/session_warmer.rbs +221 -0
- data/sig/bidi2pdf/test_helpers/testcontainers/chromedriver_test_helper.rbs +1 -1
- data/sig/bidi2pdf/version.rbs +1 -1
- data/sig/bidi2pdf.rbs +84 -0
- data/tasks/release_credentials_check.rake +97 -0
- metadata +39 -4
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
module Bidi2pdf
|
|
2
|
+
class Recipe
|
|
3
|
+
# Validates a loaded recipe against the schema `bidi2pdf schema recipe` publishes, before any
|
|
4
|
+
# browser is launched - `bidi2pdf run --validate` runs exactly this and nothing else. Every
|
|
5
|
+
# failure is Bidi2pdf::InvalidRecipeError (or Bidi2pdf::PdfInspectionUnavailableError for a
|
|
6
|
+
# PDF assertion without pdf-reader installed), details carrying a `path` an agent can act on
|
|
7
|
+
# directly.
|
|
8
|
+
class Validator
|
|
9
|
+
@recipe: untyped
|
|
10
|
+
|
|
11
|
+
def initialize: (untyped recipe) -> void
|
|
12
|
+
|
|
13
|
+
def validate!: () -> untyped
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
def check_version: () -> (nil | untyped)
|
|
18
|
+
|
|
19
|
+
def check_top_level_keys: () -> (nil | untyped)
|
|
20
|
+
|
|
21
|
+
def check_source: () -> untyped
|
|
22
|
+
|
|
23
|
+
def check_steps: (untyped section, untyped steps, untyped known) -> untyped
|
|
24
|
+
|
|
25
|
+
# Mirrors Schema::RECIPE_ACTIONS' own wait_for oneOf: exactly one of selector/paged_js/script.
|
|
26
|
+
# --validate runs this check itself rather than relying on Runner#wait_for_condition, whose
|
|
27
|
+
# own "needs one of" error only fires mid-run (after a browser is already launched) and does
|
|
28
|
+
# not catch "more than one given" at all - it just silently prefers paged_js, then selector.
|
|
29
|
+
def check_wait_for_conditions: () -> untyped
|
|
30
|
+
|
|
31
|
+
# Mirrors Schema::RECIPE_ASSERT's own const: true for the 4 presence-only assertions -
|
|
32
|
+
# Runner never reads the value beside them, so anything but `true` is misleading rather than
|
|
33
|
+
# merely unusual (see Recipe::PRESENCE_ONLY_ASSERTIONS' own comment).
|
|
34
|
+
def check_presence_assertions: () -> untyped
|
|
35
|
+
|
|
36
|
+
def check_pdf_assertions_need_pdf_inspection: () -> (nil | untyped)
|
|
37
|
+
|
|
38
|
+
def check_output: () -> (nil | untyped)
|
|
39
|
+
|
|
40
|
+
def check_print_options: () -> untyped
|
|
41
|
+
|
|
42
|
+
def symbolize: (untyped hash) -> untyped
|
|
43
|
+
|
|
44
|
+
# Runs last, deliberately: everything above gives a friendlier, more specific message for
|
|
45
|
+
# the case it already knows about (an unknown action name, a non-true presence assertion, a
|
|
46
|
+
# real PrintParametersValidator error, ...), so this only ever fires for a shape violation
|
|
47
|
+
# none of them catches - an extra/unrecognized key sitting alongside an otherwise-valid one
|
|
48
|
+
# (SchemaShape's own doc comment has the concrete examples this closes).
|
|
49
|
+
def check_shape: () -> (nil | untyped)
|
|
50
|
+
|
|
51
|
+
def fail!: (untyped reason, path: untyped) -> untyped
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
module Bidi2pdf
|
|
2
|
+
# A recipe is a rendering contract: the waits and preparation a page needs before it is printed,
|
|
3
|
+
# and the properties the resulting PDF must have - not a general browser-automation script.
|
|
4
|
+
# `bidi2pdf run recipe.yml` is Loader -> (an invalid recipe stops here, no browser ever
|
|
5
|
+
# launches) -> Validator -> Runner.
|
|
6
|
+
class Recipe
|
|
7
|
+
@data: untyped
|
|
8
|
+
|
|
9
|
+
@path: untyped
|
|
10
|
+
|
|
11
|
+
KNOWN_TOP_LEVEL_KEYS: ::Array["version" | "source" | "browser" | "headers" | "cookies" | "auth" | "actions" | "assert" | "print" | "output"]
|
|
12
|
+
|
|
13
|
+
KNOWN_ACTIONS: ::Array["wait_for" | "click" | "evaluate" | "inject_script" | "inject_style" | "set_viewport" | "wait_network_idle"]
|
|
14
|
+
|
|
15
|
+
PAGE_ASSERTIONS: ::Array["selector_exists" | "text_present" | "no_console_errors" | "no_network_failures" | "fonts_loaded"]
|
|
16
|
+
|
|
17
|
+
PDF_ASSERTIONS: ::Array["page_count" | "pdf_text_present" | "pdf_not_blank"]
|
|
18
|
+
|
|
19
|
+
KNOWN_ASSERTIONS: untyped
|
|
20
|
+
|
|
21
|
+
KNOWN_OUTPUTS: ::Array["pdf" | "manifest" | "screenshot"]
|
|
22
|
+
|
|
23
|
+
# These four assertions are pure presence checks - Runner never reads the value beside them
|
|
24
|
+
# (see #assertion_no_console_errors and friends), so a bare key is really what's being
|
|
25
|
+
# asserted. `false`/anything but `true` is therefore misleading, not merely unusual: schema
|
|
26
|
+
# recipe encodes this as `const: true` and Validator#check_presence_assertions enforces it too.
|
|
27
|
+
PRESENCE_ONLY_ASSERTIONS: ::Array["no_console_errors" | "no_network_failures" | "fonts_loaded" | "pdf_not_blank"]
|
|
28
|
+
|
|
29
|
+
attr_reader data: untyped
|
|
30
|
+
|
|
31
|
+
attr_reader path: untyped
|
|
32
|
+
|
|
33
|
+
def self.load: (untyped path) -> untyped
|
|
34
|
+
|
|
35
|
+
def initialize: (untyped data, ?path: untyped?) -> void
|
|
36
|
+
|
|
37
|
+
def validate!: () -> untyped
|
|
38
|
+
|
|
39
|
+
def actions: () -> untyped
|
|
40
|
+
|
|
41
|
+
def assertions: () -> untyped
|
|
42
|
+
|
|
43
|
+
def source: () -> untyped
|
|
44
|
+
|
|
45
|
+
def browser_options: () -> untyped
|
|
46
|
+
|
|
47
|
+
def headers: () -> untyped
|
|
48
|
+
|
|
49
|
+
def cookies: () -> untyped
|
|
50
|
+
|
|
51
|
+
def auth: () -> untyped
|
|
52
|
+
|
|
53
|
+
def print_options: () -> untyped
|
|
54
|
+
|
|
55
|
+
def output: () -> untyped
|
|
56
|
+
|
|
57
|
+
def needs_pdf?: () -> untyped
|
|
58
|
+
|
|
59
|
+
def self.step_name: (untyped step) -> untyped
|
|
60
|
+
|
|
61
|
+
def step_name: (untyped step) -> untyped
|
|
62
|
+
|
|
63
|
+
# The value beside a step's name - a Hash of options (`wait_for: {selector: ..., timeout:
|
|
64
|
+
# ...}`), a bare scalar (`page_count: 2`, `no_console_errors: true`), or nil.
|
|
65
|
+
def step_value: (untyped step) -> (untyped | nil)
|
|
66
|
+
|
|
67
|
+
# step_value coerced to a Hash of options - most actions/assertions take this shape; a bare
|
|
68
|
+
# scalar step (page_count: 2, no_console_errors: true) has no options of its own to offer here,
|
|
69
|
+
# so callers that need the scalar itself use #step_value directly.
|
|
70
|
+
def step_options: (untyped step) -> untyped
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
module Bidi2pdf
|
|
2
|
+
# Structured result of a CLI command (render/diagnose/run) - the shape behind --json output, a
|
|
3
|
+
# render manifest, and the final "result" event of --json-stream. Construct via .success/
|
|
4
|
+
# .failure rather than .new directly so every Result is unambiguously one or the other.
|
|
5
|
+
class Result
|
|
6
|
+
@command: untyped
|
|
7
|
+
|
|
8
|
+
@ok: untyped
|
|
9
|
+
|
|
10
|
+
@output: untyped
|
|
11
|
+
|
|
12
|
+
@bytes: untyped
|
|
13
|
+
|
|
14
|
+
@sha256: untyped
|
|
15
|
+
|
|
16
|
+
@pages: untyped
|
|
17
|
+
|
|
18
|
+
@duration_ms: untyped
|
|
19
|
+
|
|
20
|
+
@navigation: untyped
|
|
21
|
+
|
|
22
|
+
@console: untyped
|
|
23
|
+
|
|
24
|
+
@network_failures: untyped
|
|
25
|
+
|
|
26
|
+
@warnings: untyped
|
|
27
|
+
|
|
28
|
+
@error: untyped
|
|
29
|
+
|
|
30
|
+
@metadata: untyped
|
|
31
|
+
|
|
32
|
+
SCHEMA_VERSION: 1
|
|
33
|
+
|
|
34
|
+
attr_reader command: untyped
|
|
35
|
+
|
|
36
|
+
attr_reader output: untyped
|
|
37
|
+
|
|
38
|
+
attr_reader bytes: untyped
|
|
39
|
+
|
|
40
|
+
attr_reader sha256: untyped
|
|
41
|
+
|
|
42
|
+
attr_reader pages: untyped
|
|
43
|
+
|
|
44
|
+
attr_reader duration_ms: untyped
|
|
45
|
+
|
|
46
|
+
attr_reader navigation: untyped
|
|
47
|
+
|
|
48
|
+
attr_reader console: untyped
|
|
49
|
+
|
|
50
|
+
attr_reader network_failures: untyped
|
|
51
|
+
|
|
52
|
+
attr_reader warnings: untyped
|
|
53
|
+
|
|
54
|
+
attr_reader error: untyped
|
|
55
|
+
|
|
56
|
+
attr_reader metadata: untyped
|
|
57
|
+
|
|
58
|
+
# rubocop:disable-next Metrics/ParameterLists, Naming/MethodParameterName
|
|
59
|
+
def initialize: (command: untyped, ok: untyped, ?output: untyped?, ?bytes: untyped?, ?sha256: untyped?, ?pages: untyped?, ?duration_ms: untyped?, ?navigation: untyped?, ?console: untyped, ?network_failures: untyped, ?warnings: untyped, ?error: untyped?, ?metadata: ::Hash[untyped, untyped]) -> void
|
|
60
|
+
|
|
61
|
+
def self.success: (**untyped) -> untyped
|
|
62
|
+
|
|
63
|
+
def self.failure: (error: untyped, **untyped) -> untyped
|
|
64
|
+
|
|
65
|
+
def ok?: () -> untyped
|
|
66
|
+
|
|
67
|
+
def to_h: () -> untyped
|
|
68
|
+
|
|
69
|
+
def to_json: (*untyped) -> untyped
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
module Bidi2pdf
|
|
2
|
+
# Builds a Bidi2pdf::Result by subscribing to the gem's own instrumentation events
|
|
3
|
+
# (Bidi2pdf::Notifications) around a block that does the actual rendering, rather than changing
|
|
4
|
+
# what Launcher/SessionRunner/BrowserTab return - Launcher#launch keeps returning the raw base64
|
|
5
|
+
# PDF when no output file is given, exactly as the programmatic API already promises (see
|
|
6
|
+
# spec/acceptance/launcher_spec.rb).
|
|
7
|
+
class ResultCollector
|
|
8
|
+
@requested_url: untyped
|
|
9
|
+
|
|
10
|
+
@command: untyped
|
|
11
|
+
|
|
12
|
+
@output: untyped
|
|
13
|
+
|
|
14
|
+
@console: untyped
|
|
15
|
+
|
|
16
|
+
@network_events: untyped
|
|
17
|
+
|
|
18
|
+
@pdf_bytes: untyped
|
|
19
|
+
|
|
20
|
+
# Capture the Proc #subscribe actually stored (it converts &handler into one), so
|
|
21
|
+
# #unsubscribe below can remove exactly this registration - passing the handler positionally
|
|
22
|
+
# rather than as a second &block, since Notifications#unsubscribe(pattern, block = nil)
|
|
23
|
+
# clears *every* subscriber for that pattern when block is nil/omitted.
|
|
24
|
+
@registered: untyped
|
|
25
|
+
|
|
26
|
+
@navigation_event: untyped
|
|
27
|
+
|
|
28
|
+
# #pdf_bytes is the raw, decoded PDF (nil if nothing was printed) - not part of Result#to_h
|
|
29
|
+
# (which only carries bytesize/sha256/pages), but what a stdout-output render (--output -)
|
|
30
|
+
# writes to $stdout, and what a caller can hand to Bidi2pdf::PdfInspection itself.
|
|
31
|
+
#
|
|
32
|
+
# #console/#network_failures are exposed live (not only via the Result #around eventually
|
|
33
|
+
# returns) so a caller running inside the block - Recipe::Runner's assertions - can read them
|
|
34
|
+
# before the block itself finishes.
|
|
35
|
+
attr_reader requested_url: untyped
|
|
36
|
+
|
|
37
|
+
# #pdf_bytes is the raw, decoded PDF (nil if nothing was printed) - not part of Result#to_h
|
|
38
|
+
# (which only carries bytesize/sha256/pages), but what a stdout-output render (--output -)
|
|
39
|
+
# writes to $stdout, and what a caller can hand to Bidi2pdf::PdfInspection itself.
|
|
40
|
+
#
|
|
41
|
+
# #console/#network_failures are exposed live (not only via the Result #around eventually
|
|
42
|
+
# returns) so a caller running inside the block - Recipe::Runner's assertions - can read them
|
|
43
|
+
# before the block itself finishes.
|
|
44
|
+
attr_reader navigation_event: untyped
|
|
45
|
+
|
|
46
|
+
# #pdf_bytes is the raw, decoded PDF (nil if nothing was printed) - not part of Result#to_h
|
|
47
|
+
# (which only carries bytesize/sha256/pages), but what a stdout-output render (--output -)
|
|
48
|
+
# writes to $stdout, and what a caller can hand to Bidi2pdf::PdfInspection itself.
|
|
49
|
+
#
|
|
50
|
+
# #console/#network_failures are exposed live (not only via the Result #around eventually
|
|
51
|
+
# returns) so a caller running inside the block - Recipe::Runner's assertions - can read them
|
|
52
|
+
# before the block itself finishes.
|
|
53
|
+
attr_reader pdf_bytes: untyped
|
|
54
|
+
|
|
55
|
+
# #pdf_bytes is the raw, decoded PDF (nil if nothing was printed) - not part of Result#to_h
|
|
56
|
+
# (which only carries bytesize/sha256/pages), but what a stdout-output render (--output -)
|
|
57
|
+
# writes to $stdout, and what a caller can hand to Bidi2pdf::PdfInspection itself.
|
|
58
|
+
#
|
|
59
|
+
# #console/#network_failures are exposed live (not only via the Result #around eventually
|
|
60
|
+
# returns) so a caller running inside the block - Recipe::Runner's assertions - can read them
|
|
61
|
+
# before the block itself finishes.
|
|
62
|
+
attr_reader console: untyped
|
|
63
|
+
|
|
64
|
+
def initialize: (requested_url: untyped, command: untyped, ?output: untyped?) -> void
|
|
65
|
+
|
|
66
|
+
# Runs the block with the collector subscribed, then returns a Bidi2pdf::Result - success with
|
|
67
|
+
# whatever was captured, or failure describing whatever the block raised. Never itself raises.
|
|
68
|
+
def around: () { () -> untyped } -> untyped
|
|
69
|
+
|
|
70
|
+
def network_failures: () -> untyped
|
|
71
|
+
|
|
72
|
+
private
|
|
73
|
+
|
|
74
|
+
def now_ms: () -> untyped
|
|
75
|
+
|
|
76
|
+
def elapsed_ms: (untyped start) -> untyped
|
|
77
|
+
|
|
78
|
+
def subscribe: () -> untyped
|
|
79
|
+
|
|
80
|
+
def unsubscribe: () -> untyped
|
|
81
|
+
|
|
82
|
+
# Kept for its #duration (used by Manifest's navigation.duration_ms); Result itself doesn't
|
|
83
|
+
# surface navigation timing separately from the overall duration_ms.
|
|
84
|
+
def on_navigate: (untyped event) -> untyped
|
|
85
|
+
|
|
86
|
+
def on_console: (untyped event) -> untyped
|
|
87
|
+
|
|
88
|
+
def on_network_event: (untyped event) -> (nil | untyped)
|
|
89
|
+
|
|
90
|
+
def on_print: (untyped event) -> untyped
|
|
91
|
+
|
|
92
|
+
# rubocop:disable-next Naming/MethodParameterName
|
|
93
|
+
def build_result: (ok: untyped, duration_ms: untyped, ?error: untyped?) -> untyped
|
|
94
|
+
|
|
95
|
+
def navigation_info: () -> { requested_url: untyped, final_url: untyped, status: untyped }
|
|
96
|
+
|
|
97
|
+
# Best-effort external approximation of what BrowserTab#send(:correlated_navigation_response)
|
|
98
|
+
# already computes internally (not exposed publicly): among the network events this collector
|
|
99
|
+
# observed, the ones carrying a real BiDi "navigation" id are main-frame document requests: the
|
|
100
|
+
# one with the latest start_timestamp among those sharing the most recent such navigation id is
|
|
101
|
+
# the final hop of that navigation, and its own #url is therefore the page actually reached.
|
|
102
|
+
def final_navigation_event: () -> (nil | untyped)
|
|
103
|
+
|
|
104
|
+
def warnings: () -> (::Array[untyped] | ::Array["pages requires the pdf-reader gem"])
|
|
105
|
+
end
|
|
106
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
module Bidi2pdf
|
|
2
|
+
# JSON Schema documents behind `bidi2pdf schema <kind>` - how an agent discovers the shape of
|
|
3
|
+
# --json/--manifest/--json-stream/a recipe file without reading prose documentation. These are
|
|
4
|
+
# the source of truth for those shapes; a change to Result/Diagnose's payload/Recipe::Runner's
|
|
5
|
+
# result shape/Manifest/JsonSubscriber/Recipe's own fields should update the matching schema
|
|
6
|
+
# here in the same change. Kind names match the `command` field a response already carries
|
|
7
|
+
# (`"command": "diagnose"` -> `bidi2pdf schema diagnose`), not the Ruby class names behind them.
|
|
8
|
+
module Schema
|
|
9
|
+
ERROR: { "type" => ::Array["object" | "null"], "required" => ::Array["code" | "message" | "retryable"], "properties" => { "code" => { "type" => "string" }, "message" => { "type" => "string" }, "retryable" => { "type" => "boolean" }, "hint" => { "type" => ::Array["string" | "null"] }, "details" => { "type" => "object" } } }
|
|
10
|
+
|
|
11
|
+
NAVIGATION: { "type" => ::Array["object" | "null"], "properties" => { "requested_url" => { "type" => ::Array["string" | "null"] }, "final_url" => { "type" => ::Array["string" | "null"] }, "status" => { "type" => ::Array["integer" | "null"] } } }
|
|
12
|
+
|
|
13
|
+
CONSOLE: { "type" => "array", "items" => { "type" => "object", "properties" => { "level" => { "type" => "string" }, "text" => { "type" => "string" } } } }
|
|
14
|
+
|
|
15
|
+
NETWORK_FAILURES: { "type" => "array", "items" => { "type" => "object", "properties" => { "url" => { "type" => "string" }, "method" => { "type" => ::Array["string" | "null"] }, "status" => { "type" => ::Array["integer" | "null"] }, "state" => { "type" => "string" } } } }
|
|
16
|
+
|
|
17
|
+
WARNINGS: { "type" => "array", "items" => { "type" => "string" } }
|
|
18
|
+
|
|
19
|
+
RENDER: { "$schema" => "https://json-schema.org/draft/2020-12/schema", "title" => "Bidi2pdf::Result (render)", "description" => "--json output of `bidi2pdf render`, and the final \"result\" event of its --json-stream", "type" => "object", "required" => ::Array["schema_version" | "ok" | "command" | "console" | "network_failures" | "warnings"], "properties" => { "schema_version" => { "const" => 1 }, "ok" => { "type" => "boolean" }, "command" => { "const" => "render" }, "output" => { "type" => ::Array["string" | "null"] }, "bytes" => { "type" => ::Array["integer" | "null"] }, "sha256" => { "type" => ::Array["string" | "null"] }, "pages" => { "type" => ::Array["integer" | "null"], "description" => "null when the pdf-reader gem is unavailable" }, "duration_ms" => { "type" => ::Array["integer" | "null"] }, "navigation" => untyped, "console" => untyped, "network_failures" => untyped, "warnings" => untyped, "error" => untyped } }
|
|
20
|
+
|
|
21
|
+
DIAGNOSE: { "$schema" => "https://json-schema.org/draft/2020-12/schema", "title" => "bidi2pdf diagnose result", "description" => "--json output of `bidi2pdf diagnose`, and the final \"result\" event of its --json-stream. page/fonts/print_media/paged_js are Bidi2pdf::Diagnose's own parsed script results, loosely shaped here rather than over-specified.", "type" => "object", "required" => ::Array["schema_version" | "ok" | "command" | "console" | "network_failures" | "warnings"], "properties" => { "schema_version" => { "const" => 1 }, "ok" => { "type" => "boolean" }, "command" => { "const" => "diagnose" }, "page" => { "type" => ::Array["object" | "null"] }, "console" => untyped, "network_failures" => untyped, "fonts" => { "type" => ::Array["object" | "null"] }, "print_media" => { "type" => ::Array["object" | "null"] }, "paged_js" => { "type" => ::Array["object" | "null"] }, "screenshot" => { "type" => ::Array["string" | "null"], "description" => "path written, when --screenshot was given" }, "pdf" => { "type" => ::Array["object" | "null"], "description" => "present when --pdf was given", "properties" => { "path" => { "type" => "string" }, "pages" => { "type" => ::Array["integer" | "null"] }, "not_blank" => { "type" => ::Array["boolean" | "null"] } } }, "warnings" => untyped, "error" => untyped } }
|
|
22
|
+
|
|
23
|
+
RUN_STEP: { "type" => "object", "required" => ::Array["index" | "type" | "ok"], "properties" => { "index" => { "type" => "integer" }, "type" => { "type" => "string" }, "ok" => { "type" => "boolean" }, "duration_ms" => { "type" => "integer" }, "details" => { "type" => "object" } } }
|
|
24
|
+
|
|
25
|
+
RUN: { "$schema" => "https://json-schema.org/draft/2020-12/schema", "title" => "bidi2pdf run result", "description" => "--json output of `bidi2pdf run recipe.yml`, and the final \"result\" event of its --json-stream", "type" => "object", "required" => ::Array["schema_version" | "ok" | "command" | "recipe" | "actions" | "assertions" | "warnings"], "properties" => { "schema_version" => { "const" => 1 }, "ok" => { "type" => "boolean" }, "command" => { "const" => "run" }, "recipe" => { "type" => "string", "description" => "the recipe file path given on the command line" }, "actions" => { "type" => "array", "items" => untyped }, "assertions" => { "type" => "array", "items" => untyped }, "assigned" => { "type" => "object", "description" => "values captured by evaluate ... assign:, keyed by name" }, "output" => { "type" => ::Array["object" | "null"], "description" => "null when the run failed", "properties" => { "pdf" => { "type" => ::Array["string" | "null"] }, "manifest" => { "type" => ::Array["string" | "null"] }, "screenshot" => { "type" => ::Array["string" | "null"] }, "bytes" => { "type" => ::Array["integer" | "null"] }, "sha256" => { "type" => ::Array["string" | "null"] }, "pages" => { "type" => ::Array["integer" | "null"] } } }, "duration_ms" => { "type" => ::Array["integer" | "null"] }, "warnings" => untyped, "error" => untyped } }
|
|
26
|
+
|
|
27
|
+
MANIFEST: { "$schema" => "https://json-schema.org/draft/2020-12/schema", "title" => "Bidi2pdf render manifest", "description" => "--manifest FILE output", "type" => "object", "required" => ::Array["schema_version" | "bidi2pdf_version" | "created_at" | "input" | "output" | "navigation" | "render"], "properties" => { "schema_version" => { "const" => 1 }, "bidi2pdf_version" => { "type" => "string" }, "created_at" => { "type" => "string", "format" => "date-time" }, "input" => { "type" => "object", "required" => ::Array["type"], "properties" => { "type" => { "type" => "string", "enum" => ::Array["url" | "html_file" | "stdin"] }, "url" => { "type" => ::Array["string" | "null"] } } }, "output" => { "type" => "object", "properties" => { "path" => { "type" => ::Array["string" | "null"] }, "pages" => { "type" => ::Array["integer" | "null"] }, "bytes" => { "type" => ::Array["integer" | "null"] }, "sha256" => { "type" => ::Array["string" | "null"] } } }, "browser" => { "type" => "object" }, "navigation" => untyped, "render" => { "type" => "object", "properties" => { "duration_ms" => { "type" => ::Array["integer" | "null"] } } }, "console" => untyped, "network_failures" => untyped, "warnings" => untyped, "headers" => { "type" => "object", "description" => "present only when custom headers were given; secret-bearing values are redacted" } } }
|
|
28
|
+
|
|
29
|
+
EVENT: { "$schema" => "https://json-schema.org/draft/2020-12/schema", "title" => "Bidi2pdf --json-stream event", "description" => "one JSON object per stderr line while --json-stream is active. In structured mode (--json/--output -), the last line is always {event: \"result\", result: <the matching command's own schema - see \"result\"'s own oneOf>}. In human mode (--json-stream given alone), only progress events are emitted - there is no synthetic final \"result\" event, since there is no structured result being built; the human-readable log is the completion/failure signal.", "type" => "object", "required" => ::Array["schema_version" | "t_ms" | "event"], "properties" => { "schema_version" => { "const" => 1 }, "t_ms" => { "type" => "integer", "description" => "milliseconds since the render started" }, "event" => { "type" => "string", "enum" => ::Array["navigate" | "page_loaded" | "network_idle" | "console" | "print" | "screenshot" | "result"] }, "url" => { "type" => "string", "description" => "present on a navigate event" }, "level" => { "type" => "string", "description" => "present on a console event" }, "text" => { "type" => "string", "description" => "present on a console event" }, "result" => { "description" => "present only on the final \"result\" event (structured mode only); shape depends on which command produced this stream - each branch's own \"command\" value disambiguates", "oneOf" => ::Array[untyped] } } }
|
|
30
|
+
|
|
31
|
+
RECIPE_SOURCE: { "description" => "exactly one of url, file, stdin", "oneOf" => ::Array[{ "type" => "object", "required" => ::Array["url"], "properties" => { "url" => { "type" => "string" } }, "additionalProperties" => false } | { "type" => "object", "required" => ::Array["file"], "properties" => { "file" => { "type" => "string" } }, "additionalProperties" => false } | { "type" => "object", "required" => ::Array["stdin"], "properties" => { "stdin" => { "const" => true } }, "additionalProperties" => false }] }
|
|
32
|
+
|
|
33
|
+
RECIPE_ACTIONS: { "description" => "one of the 7 known actions - a thin dispatch over BrowserTab's own methods, see README's Agent and Automation Usage section", "oneOf" => ::Array[{ "type" => "object", "required" => ::Array["wait_for"], "additionalProperties" => false, "properties" => { "wait_for" => { "type" => "object", "description" => "exactly one of selector, paged_js, script; timeout is always optional", "oneOf" => ::Array[{ "type" => "object", "required" => ::Array["selector"], "additionalProperties" => false, "properties" => { "selector" => { "type" => "string" }, "timeout" => { "type" => "number" } } } | { "type" => "object", "required" => ::Array["paged_js"], "additionalProperties" => false, "properties" => { "paged_js" => { "type" => "boolean" }, "timeout" => { "type" => "number" } } } | { "type" => "object", "required" => ::Array["script"], "additionalProperties" => false, "properties" => { "script" => { "type" => "string" }, "timeout" => { "type" => "number" } } }] } } } | { "type" => "object", "required" => ::Array["click"], "additionalProperties" => false, "properties" => { "click" => { "type" => "object", "required" => ::Array["selector"], "additionalProperties" => false, "properties" => { "selector" => { "type" => "string" } } } } } | { "type" => "object", "required" => ::Array["evaluate"], "additionalProperties" => false, "properties" => { "evaluate" => { "type" => "object", "required" => ::Array["script"], "additionalProperties" => false, "properties" => { "script" => { "type" => "string" }, "assign" => { "type" => "string" } } } } } | { "type" => "object", "required" => ::Array["inject_script"], "additionalProperties" => false, "properties" => { "inject_script" => { "type" => "object", "additionalProperties" => false, "properties" => { "url" => { "type" => "string" }, "content" => { "type" => "string" }, "id" => { "type" => "string" } } } } } | { "type" => "object", "required" => ::Array["inject_style"], "additionalProperties" => false, "properties" => { "inject_style" => { "type" => "object", "additionalProperties" => false, "properties" => { "url" => { "type" => "string" }, "content" => { "type" => "string" }, "id" => { "type" => "string" } } } } } | { "type" => "object", "required" => ::Array["set_viewport"], "additionalProperties" => false, "properties" => { "set_viewport" => { "type" => "object", "required" => ::Array["width" | "height"], "additionalProperties" => false, "properties" => { "width" => { "type" => "number" }, "height" => { "type" => "number" }, "device_pixel_ratio" => { "type" => "number" } } } } } | { "type" => "object", "required" => ::Array["wait_network_idle"], "additionalProperties" => false, "properties" => { "wait_network_idle" => { "type" => "object", "additionalProperties" => false, "properties" => { "timeout" => { "type" => "number" } } } } }] }
|
|
34
|
+
|
|
35
|
+
RECIPE_ASSERT: { "description" => "one of the 8 known assertions. The presence-only ones (no_console_errors, no_network_failures, fonts_loaded, pdf_not_blank) never have their value read at run time - Recipe::Runner only checks the key is there - so they must be written as `true`; `false` or any other value is rejected as misleading rather than silently ignored. The last 3 assertions also require the pdf-reader gem at --validate time - a runtime fact this schema cannot express, see the top-level description", "oneOf" => ::Array[{ "type" => "object", "required" => ::Array["selector_exists"], "additionalProperties" => false, "properties" => { "selector_exists" => { "type" => "object", "required" => ::Array["selector"], "additionalProperties" => false, "properties" => { "selector" => { "type" => "string" } } } } } | { "type" => "object", "required" => ::Array["text_present"], "additionalProperties" => false, "properties" => { "text_present" => { "type" => "object", "required" => ::Array["text"], "additionalProperties" => false, "properties" => { "text" => { "type" => "string" } } } } } | { "type" => "object", "required" => ::Array["no_console_errors"], "additionalProperties" => false, "properties" => { "no_console_errors" => { "const" => true } } } | { "type" => "object", "required" => ::Array["no_network_failures"], "additionalProperties" => false, "properties" => { "no_network_failures" => { "const" => true } } } | { "type" => "object", "required" => ::Array["fonts_loaded"], "additionalProperties" => false, "properties" => { "fonts_loaded" => { "const" => true } } } | { "type" => "object", "required" => ::Array["page_count"], "additionalProperties" => false, "properties" => { "page_count" => { "oneOf" => ::Array[{ "type" => "integer" } | { "type" => "object", "properties" => { "min" => { "type" => "integer" }, "max" => { "type" => "integer" } }, "additionalProperties" => false }] } } } | { "type" => "object", "required" => ::Array["pdf_text_present"], "additionalProperties" => false, "properties" => { "pdf_text_present" => { "type" => "object", "required" => ::Array["text"], "additionalProperties" => false, "properties" => { "text" => { "type" => "string" } } } } } | { "type" => "object", "required" => ::Array["pdf_not_blank"], "additionalProperties" => false, "properties" => { "pdf_not_blank" => { "const" => true } } }] }
|
|
36
|
+
|
|
37
|
+
RECIPE_PRINT: { "type" => "object", "description" => "the same options `bidi2pdf render`'s own print flags map to; validated by Bidi::Commands::PrintParametersValidator", "properties" => { "background" => { "type" => "boolean" }, "shrinkToFit" => { "type" => "boolean" }, "orientation" => { "type" => "string", "enum" => ::Array["portrait" | "landscape"] }, "scale" => { "type" => "number", "minimum" => ::Float, "maximum" => ::Float }, "pageRanges" => { "type" => "array", "items" => { "type" => ::Array["integer" | "string"] } }, "margin" => { "type" => "object", "properties" => { "top" => { "type" => "number", "minimum" => 0 }, "bottom" => { "type" => "number", "minimum" => 0 }, "left" => { "type" => "number", "minimum" => 0 }, "right" => { "type" => "number", "minimum" => 0 } } }, "page" => { "type" => "object", "properties" => { "format" => { "type" => "string" }, "width" => { "type" => "number", "minimum" => ::Float }, "height" => { "type" => "number", "minimum" => ::Float } } } } }
|
|
38
|
+
|
|
39
|
+
RECIPE: { "$schema" => "https://json-schema.org/draft/2020-12/schema", "title" => "Bidi2pdf recipe", "description" => "input to `bidi2pdf run recipe.yml` (YAML or JSON). This schema is meant to be sufficient on its own to generate a recipe `--validate` will accept, with one exception JSON Schema cannot express: a page_count/pdf_text_present/pdf_not_blank assertion also requires the pdf-reader gem to be installed at validation time (a runtime environment fact, not a document-shape one) - `--validate` still catches a violation, with error code PDF_INSPECTION_UNAVAILABLE.", "type" => "object", "required" => ::Array["version" | "source" | "output"], "additionalProperties" => false, "properties" => { "version" => { "const" => 1 }, "source" => untyped, "browser" => { "type" => "object", "properties" => { "headless" => { "type" => "boolean" } } }, "headers" => { "type" => "object" }, "cookies" => { "type" => "object" }, "auth" => { "type" => "object", "properties" => { "username" => { "type" => "string" }, "password" => { "type" => "string" } } }, "actions" => { "type" => "array", "items" => untyped }, "assert" => { "type" => "array", "items" => untyped }, "print" => untyped, "output" => { "type" => "object", "description" => "at least one of pdf, manifest, screenshot", "anyOf" => ::Array[{ "required" => ::Array["pdf"] } | { "required" => ::Array["manifest"] } | { "required" => ::Array["screenshot"] }], "properties" => { "pdf" => { "type" => "string" }, "manifest" => { "type" => "string" }, "screenshot" => { "type" => "string" } } } } }
|
|
40
|
+
|
|
41
|
+
ALL: { "render" => untyped, "diagnose" => untyped, "run" => untyped, "manifest" => untyped, "recipe" => untyped, "event" => untyped }
|
|
42
|
+
|
|
43
|
+
def self.for: (untyped kind) -> untyped
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -67,6 +67,17 @@ module Bidi2pdf
|
|
|
67
67
|
|
|
68
68
|
def run: () -> untyped
|
|
69
69
|
|
|
70
|
+
# Like #run, but stops after navigation (and the configured waits) instead of printing, and
|
|
71
|
+
# leaves the tab open for the caller to inspect - used by `bidi2pdf diagnose`, which has no PDF
|
|
72
|
+
# to produce. The caller is responsible for #close_all once done.
|
|
73
|
+
#
|
|
74
|
+
# @return [Bidi2pdf::Bidi::BrowserTab] the navigated tab.
|
|
75
|
+
def run_diagnose: () -> untyped
|
|
76
|
+
|
|
77
|
+
# Closes what #run_diagnose opened. #run's own run_flow already does this itself (in its
|
|
78
|
+
# ensure), so this is only ever needed after #run_diagnose.
|
|
79
|
+
def close_all: () -> untyped
|
|
80
|
+
|
|
70
81
|
private
|
|
71
82
|
|
|
72
83
|
def setup_browser: () -> untyped
|
|
@@ -77,6 +88,12 @@ module Bidi2pdf
|
|
|
77
88
|
|
|
78
89
|
def add_basic_auth: () -> (nil | untyped)
|
|
79
90
|
|
|
91
|
+
# Navigate + wait only, no log_network_traffic/print side effects - #run_flow above
|
|
92
|
+
# intentionally keeps its own inline sequence (where log_network_traffic sits between the two
|
|
93
|
+
# waits) untouched rather than sharing this, since that render-only file-writing side effect
|
|
94
|
+
# has no place in a diagnose flow.
|
|
95
|
+
def navigate_and_wait: () -> untyped
|
|
96
|
+
|
|
80
97
|
# rubocop: disable Metrics/AbcSize
|
|
81
98
|
def run_flow: () -> untyped
|
|
82
99
|
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
module Bidi2pdf
|
|
2
|
+
# Keeps a small number of Chrome sessions pre-warmed (chromedriver started, WebSocket connected,
|
|
3
|
+
# browser ready) so a render can skip that startup latency on the request path. Isolation matches
|
|
4
|
+
# today's one-Chrome-per-render model exactly: every checked-out slot is used for exactly one
|
|
5
|
+
# +with_tab+ block and then retired (never returned to the cache) - only a fresh replacement is
|
|
6
|
+
# warmed in its place, off the request path. Checkout never waits for a warm slot: one is used if
|
|
7
|
+
# it is ready, otherwise a slot is created synchronously on the spot, i.e. today's exact behavior
|
|
8
|
+
# for that one render - including its failure mode: if that cold start fails, the error propagates
|
|
9
|
+
# out of +with_tab+ exactly as it would without the warmer.
|
|
10
|
+
#
|
|
11
|
+
# @example Rails initializer
|
|
12
|
+
# Bidi2pdf::SessionWarmer.configure do |c|
|
|
13
|
+
# c.size = 2
|
|
14
|
+
# c.headless = true
|
|
15
|
+
# end
|
|
16
|
+
#
|
|
17
|
+
# @example Per-request usage
|
|
18
|
+
# Bidi2pdf::SessionWarmer.with_tab do |tab|
|
|
19
|
+
# tab.navigate_to(url)
|
|
20
|
+
# tab.print("invoice.pdf")
|
|
21
|
+
# end
|
|
22
|
+
class SessionWarmer
|
|
23
|
+
self.@config: untyped
|
|
24
|
+
|
|
25
|
+
# Cleared first: if the constructor below raises, a stale, already-shut-down instance must
|
|
26
|
+
# not stay registered (it would keep serving cold slots but never warm again).
|
|
27
|
+
self.@instance: untyped
|
|
28
|
+
|
|
29
|
+
@config: untyped
|
|
30
|
+
|
|
31
|
+
@slot_factory: untyped
|
|
32
|
+
|
|
33
|
+
@mutex: untyped
|
|
34
|
+
|
|
35
|
+
@available: untyped
|
|
36
|
+
|
|
37
|
+
@replenish_threads: untyped
|
|
38
|
+
|
|
39
|
+
@warming: untyped
|
|
40
|
+
|
|
41
|
+
@shutdown: untyped
|
|
42
|
+
|
|
43
|
+
@reaper_wakeup: untyped
|
|
44
|
+
|
|
45
|
+
@reaper: untyped
|
|
46
|
+
|
|
47
|
+
# Configuration for the session warmer.
|
|
48
|
+
class Configuration
|
|
49
|
+
@size: untyped
|
|
50
|
+
|
|
51
|
+
@headless: untyped
|
|
52
|
+
|
|
53
|
+
@chrome_args: untyped
|
|
54
|
+
|
|
55
|
+
@remote_browser_url: untyped
|
|
56
|
+
|
|
57
|
+
@slot_factory: untyped
|
|
58
|
+
|
|
59
|
+
@max_idle_age: untyped
|
|
60
|
+
|
|
61
|
+
# @return [Integer] Number of Chrome slots to keep pre-warmed.
|
|
62
|
+
attr_accessor size: untyped
|
|
63
|
+
|
|
64
|
+
# @return [Boolean] Whether to run Chrome in headless mode.
|
|
65
|
+
attr_accessor headless: untyped
|
|
66
|
+
|
|
67
|
+
# @return [Array<String>] Chrome launch arguments.
|
|
68
|
+
attr_accessor chrome_args: untyped
|
|
69
|
+
|
|
70
|
+
# @return [String, nil] A remote chromedriver session URL (e.g. a `remote-chrome` sidecar). When
|
|
71
|
+
# set, a slot connects directly to it instead of spawning a local ChromedriverManager -
|
|
72
|
+
# mirrors Launcher#session's own local/remote branch.
|
|
73
|
+
attr_accessor remote_browser_url: untyped
|
|
74
|
+
|
|
75
|
+
# @return [#call, nil] Optional factory callable that returns a slot hash - injectable for tests.
|
|
76
|
+
# A custom factory owns the cleanup of anything it half-built before raising: the warmer can
|
|
77
|
+
# only retire a slot it was actually handed.
|
|
78
|
+
attr_accessor slot_factory: untyped
|
|
79
|
+
|
|
80
|
+
# @return [Numeric, nil] Seconds a warm slot may sit unused before it is retired and replaced.
|
|
81
|
+
# A warm slot is an open, unauthenticated automation endpoint (chromedriver's port, Chrome's
|
|
82
|
+
# debugging port) for as long as it idles, so that window is bounded by default. A slot older
|
|
83
|
+
# than this is never handed out, and a background reaper recycles idle ones within roughly
|
|
84
|
+
# 1.25x this value even when no render ever comes. +nil+ disables the limit.
|
|
85
|
+
attr_accessor max_idle_age: untyped
|
|
86
|
+
|
|
87
|
+
DEFAULT_MAX_IDLE_AGE: 300
|
|
88
|
+
|
|
89
|
+
def initialize: () -> void
|
|
90
|
+
|
|
91
|
+
# @raise [ArgumentError] if a setting can't be honored - checked once, at construction.
|
|
92
|
+
def validate!: () -> untyped
|
|
93
|
+
|
|
94
|
+
private
|
|
95
|
+
|
|
96
|
+
def validate_size!: () -> (nil | untyped)
|
|
97
|
+
|
|
98
|
+
def validate_max_idle_age!: () -> (nil | untyped)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Configures the warmer and eagerly (re)creates the singleton, warming config.size slots right
|
|
102
|
+
# here - at boot/configuration time, off the request path - instead of lazily on whichever
|
|
103
|
+
# request happens to trigger the first #with_tab (which would otherwise pay for every
|
|
104
|
+
# configured slot, serially, on that one unlucky request).
|
|
105
|
+
def self.configure: () ?{ (untyped) -> untyped } -> untyped
|
|
106
|
+
|
|
107
|
+
# Returns the warmer's configuration, initializing defaults if needed.
|
|
108
|
+
def self.config: () -> untyped
|
|
109
|
+
|
|
110
|
+
# Returns a callable that creates one real, fully-warmed Chrome slot from +config+. If building
|
|
111
|
+
# fails part-way (chromedriver up, but the session or its browser never came ready), whatever
|
|
112
|
+
# already exists is retired before the error propagates - no caller ever gets a reference to a
|
|
113
|
+
# half-built slot, so nobody else could clean it up.
|
|
114
|
+
def self.default_slot_factory: (untyped config) -> untyped
|
|
115
|
+
|
|
116
|
+
# Closes a slot's session and stops its chromedriver (nil for a remote slot, or for a part that
|
|
117
|
+
# was never created). Each step is independent: a failure in one is logged, never raised, and
|
|
118
|
+
# never skips the other.
|
|
119
|
+
def self.retire_slot: (session: untyped, manager: untyped) -> untyped
|
|
120
|
+
|
|
121
|
+
def self.safe_close: (untyped label) { () -> untyped } -> untyped
|
|
122
|
+
|
|
123
|
+
# Returns the shared singleton warmer instance, creating it (and pre-warming it) on first call.
|
|
124
|
+
def self.instance: () -> untyped
|
|
125
|
+
|
|
126
|
+
# Checks out a slot, yields a fresh tab for one render, then retires the slot.
|
|
127
|
+
def self.with_tab: () { (?) -> untyped } -> untyped
|
|
128
|
+
|
|
129
|
+
# Retires every currently-warm spare and resets the singleton.
|
|
130
|
+
def self.shutdown: () -> untyped
|
|
131
|
+
|
|
132
|
+
private
|
|
133
|
+
|
|
134
|
+
# Records each part in +parts+ the moment it exists, so #default_slot_factory's rescue can
|
|
135
|
+
# retire exactly what was created so far. Mirrors Launcher#session's local/remote branch.
|
|
136
|
+
def self.build_slot: (untyped config, untyped parts) -> { session: untyped, browser: untyped, manager: untyped }
|
|
137
|
+
|
|
138
|
+
def self.connect_remote: (untyped config, untyped parts) -> untyped
|
|
139
|
+
|
|
140
|
+
def self.start_local: (untyped config, untyped parts) -> untyped
|
|
141
|
+
|
|
142
|
+
public
|
|
143
|
+
|
|
144
|
+
def initialize: (untyped config, ?slot_factory: untyped?) -> void
|
|
145
|
+
|
|
146
|
+
# Checks out a slot, creates an isolated UserContext/Window/Tab for one render, yields the tab,
|
|
147
|
+
# then unconditionally closes those resources and retires the underlying slot.
|
|
148
|
+
def with_tab: () { (untyped) -> untyped } -> untyped
|
|
149
|
+
|
|
150
|
+
# Retires every currently-warm spare and waits for any in-flight background replenishment to
|
|
151
|
+
# finish (each of those, seeing @shutdown, retires its own result instead of stashing it - see
|
|
152
|
+
# #stash_or_retire). A subsequent #with_tab still works - it just falls back to a synchronous
|
|
153
|
+
# slot, since checkout never depends on a warm one being there.
|
|
154
|
+
def shutdown: () -> untyped
|
|
155
|
+
|
|
156
|
+
private
|
|
157
|
+
|
|
158
|
+
# Fail-fast on purpose (a Chrome that can't start at boot should be loud), but not leaky: if slot
|
|
159
|
+
# N fails, no instance is returned to own slots 1..N-1, so they are retired here first.
|
|
160
|
+
def prewarm: () -> untyped
|
|
161
|
+
|
|
162
|
+
def create_slot: () -> untyped
|
|
163
|
+
|
|
164
|
+
# Session#started? is just an internal flag set once at startup - it never flips back if Chrome,
|
|
165
|
+
# ChromeDriver, or the WebSocket dies externally while a slot sits idle in the cache. The
|
|
166
|
+
# client's own #open? is kept live by the reader thread noticing a real socket error, so
|
|
167
|
+
# checking it too catches that case - cheap (no network round trip), though still a heuristic,
|
|
168
|
+
# not a full liveness guarantee (a stuck-but-not-yet-disconnected socket still reads healthy).
|
|
169
|
+
def healthy?: (untyped slot) -> untyped
|
|
170
|
+
|
|
171
|
+
def now: () -> untyped
|
|
172
|
+
|
|
173
|
+
# Marks the moment a slot entered the cache - idle age counts from here, not from when its
|
|
174
|
+
# Chrome started, since it is the unattended waiting that max_idle_age bounds.
|
|
175
|
+
def stamp: (untyped slot) -> untyped
|
|
176
|
+
|
|
177
|
+
def expired?: (untyped slot) -> untyped
|
|
178
|
+
|
|
179
|
+
# Checkout alone can't bound idle time: with no traffic nothing would ever look at a spare. This
|
|
180
|
+
# thread does, four times per max_idle_age, so an unused slot is recycled within ~1.25x of it.
|
|
181
|
+
# Thread::Queue#pop(timeout:) doubles as an interruptible sleep - #shutdown pushes :stop.
|
|
182
|
+
def reap_loop: () -> untyped
|
|
183
|
+
|
|
184
|
+
def recycle_expired: () -> untyped
|
|
185
|
+
|
|
186
|
+
# A warm hit takes the spare and triggers a background replacement; a miss (empty cache, or a
|
|
187
|
+
# spare that died while idle) falls straight through to a synchronous slot - it never waits, so
|
|
188
|
+
# an under-provisioned warmer is never worse than not having one. A cold start that fails raises,
|
|
189
|
+
# as it would without the warmer.
|
|
190
|
+
#
|
|
191
|
+
# Oldest spare first (shift, not pop), so no slot lingers at the bottom of the cache while newer
|
|
192
|
+
# ones are used; and one past max_idle_age is never handed out, even if the reaper hasn't got to
|
|
193
|
+
# it yet - it is retired like a dead spare and this render cold-starts instead.
|
|
194
|
+
def checkout: () -> untyped
|
|
195
|
+
|
|
196
|
+
def cold_checkout: (untyped dead_slot) -> untyped
|
|
197
|
+
|
|
198
|
+
# Tops the cache up towards config.size, counting warmers already in flight. Deficit-based on
|
|
199
|
+
# purpose, not "replace what this checkout popped": that rule could never recover from a single
|
|
200
|
+
# failed warm (nothing stashed -> every later checkout a miss -> never replenished again), while
|
|
201
|
+
# replenishing unconditionally let @available grow without bound under a burst of misses.
|
|
202
|
+
# available + warming never exceeds config.size, and a failed warm frees its reservation, so the
|
|
203
|
+
# next checkout simply tries again.
|
|
204
|
+
#
|
|
205
|
+
# Threads are created and registered inside the same critical section, so #shutdown's snapshot
|
|
206
|
+
# can't miss one that has started but isn't listed yet (#warm_one needs this mutex to finish,
|
|
207
|
+
# so it just waits for it).
|
|
208
|
+
def replenish_async: () -> untyped
|
|
209
|
+
|
|
210
|
+
def warm_one: () -> untyped
|
|
211
|
+
|
|
212
|
+
# A replacement warmed after #shutdown has nothing to stash into - retire it immediately rather
|
|
213
|
+
# than leaking a live Chrome process that nothing will ever check out. Either way this warmer's
|
|
214
|
+
# reservation is released here, in the same critical section as the stash.
|
|
215
|
+
def stash_or_retire: (untyped slot) -> untyped
|
|
216
|
+
|
|
217
|
+
def retire: (untyped slot) -> untyped
|
|
218
|
+
|
|
219
|
+
def safe_close: (untyped label) { (?) -> untyped } -> untyped
|
|
220
|
+
end
|
|
221
|
+
end
|
|
@@ -41,5 +41,5 @@ class Object
|
|
|
41
41
|
|
|
42
42
|
def test_of_kind_present?: (untyped type) -> untyped
|
|
43
43
|
|
|
44
|
-
def start_chromedriver_container: (build_dir: untyped, mounts: untyped, shared_network: untyped) -> untyped
|
|
44
|
+
def start_chromedriver_container: (build_dir: untyped, mounts: untyped, shared_network: untyped, ?chromedriver_log_level: ::String) -> untyped
|
|
45
45
|
end
|
data/sig/bidi2pdf/version.rbs
CHANGED