From 6d22642d7731a3948ff299bfa38a06f0e52a631a Mon Sep 17 00:00:00 2001 From: Titus Fortner Date: Sat, 20 Jun 2026 19:18:09 -0500 Subject: [PATCH 1/5] [docs] add BiDi generated protocol layer design decision record --- .../nnnnn-bidi-generated-protocol-layer.md | 101 ++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 docs/decisions/nnnnn-bidi-generated-protocol-layer.md diff --git a/docs/decisions/nnnnn-bidi-generated-protocol-layer.md b/docs/decisions/nnnnn-bidi-generated-protocol-layer.md new file mode 100644 index 0000000000000..147766e6621b3 --- /dev/null +++ b/docs/decisions/nnnnn-bidi-generated-protocol-layer.md @@ -0,0 +1,101 @@ +# NNNNN. BiDi's low-level definitions are generated from a shared spec model and imported, not injected + +- Status: Proposed +- Discussion: _PR pending_ + +## Context + +The WebDriver BiDi specification is defined in CDDL (Concise Data Definition Language). +A binding's BiDi support spans four layers, from the wire up: + +- **Transport / session substrate** — the connection, sending each command and correlating its + response by the envelope id, and delivering inbound event frames upward. Domain-blind. +- **Low-level definitions** — the types, command shapes, and event shapes the spec defines, + including the id types (a subscription, an intercept) and the commands that produce and consume + them. +- **Orchestration** — the code that composes the definitions into capabilities and manages their + lifecycle: storing a subscription to unsubscribe later, mapping an intercept id to a handler, + matching an event to its registered callback, and wrapping events into the objects a handler + receives. +- **High-level API** — the protocol-neutral, idiomatic capabilities users program against. + +Bindings differ today in how they produce the definitions and how cleanly they separate them from +the orchestration above: + +| Binding | Current behavior | +|------------|------------------| +| Java | Hand-written (~143 module classes); definitions and orchestration on the same class; separate protocol-neutral high-level (`RemoteNetwork`). | +| Python | Generated from CDDL; orchestration injected into the generated classes via an enhancements manifest. | +| Ruby | Hand-written low-level; orchestration fused with the high-level API. | +| .NET | Hand-written module classes. | +| JavaScript | Generated from CDDL; orchestration injected into the generated classes via an enhancements manifest. | + +They also differ on source of truth — the CDDL spec or the existing implementation — and on whether +they share one model or each interpret CDDL independently. + +This record decides the low-level definitions layer — what it is, where it comes from, and how the +layers around it relate to it. This ADR assumes #17670 is accepted and that BiDi is an internal +implementation. + +## Decision + +**1. The spec is the oracle, through one shared model.** The definitions are generated from a +single shared, binding-neutral projection of the spec — not reconstructed from the existing +implementation, and not parsed independently per binding. Because the spec, not the existing code, +defines the shape, the generated definitions need not match the existing implementation — in API +shape or byte-for-byte. The shared model is the one place the spec is interpreted and normalized, so +bindings stay consistent with each other; each still emits its own language-idiomatic code from it. + +**2. The generated definitions are read-only data, not the actionable surface.** Where a generated +object reaches a user — for example, the request object inside a network handler — it is immutable, +informational data; users act through the orchestration wrapper, not on the generated object. + +**3. Orchestration stays out of the low-level definitions.** The definitions carry the spec — types, +commands, and events — and nothing that coordinates them: subscription lifecycle, event dispatch, +mapping ids to handlers, and the objects handed to a handler live in a separate layer that imports +the definitions, not spliced into the generated classes (e.g. through an enhancements manifest). A +thin, stateless convenience over a single command is a lesser matter; what must not land here is +coordination. The definitions stay a projection of the spec, so regenerating them never disturbs the +layer that imports them, and they depend only on the transport's send-and-deliver interface. + +## Considered options + +- **Keep hand-maintaining the definitions** — each binding writes and updates the protocol types, + commands, and events by hand, with no generation. + - The same protocol is hand-maintained separately in every binding, so they drift apart — the + inconsistency this record exists to prevent. + - Every spec change is a manual edit repeated in each binding, with no shared source of truth. + +- **Put the orchestration in the generated low-level class (e.g. via an enhancements manifest)** — + splice the coordination — subscription lifecycle, dispatch, handler wrapping — into the generated + classes alongside the spec types, as Python and JavaScript do today. + - Couples two layers that change for different reasons: a spec update and a coordination change + touch the same artifact, and regenerating risks the coordination. + - Coordination is harder to find, review, and type-check when it lives inside generated output. + - Thin conveniences are not the concern — the objection is coordination in the low-level layer. + +- **Derive the definitions from the existing implementation** — generate to + reproduce the current shape. + - Treats the existing implementation as the source of truth instead of the spec, carrying its + inconsistencies forward. + - Nothing supported depends on the generated definitions, so they need not match it. + +- **Generate each binding independently from CDDL** — every binding interprets the spec on its own + (or hand-writes), with no shared model. + - Each binding normalizes types, names, and gap handling separately, so the bindings drift apart + over time — the cross-binding inconsistency this record exists to prevent. + - A shared, binding-neutral model avoids the drift while still letting each binding emit + language-idiomatic code from it: the source model is shared, the generated definitions are + per-language. + +## Consequences + +- The supported API depends only on the wrapper, not on the generated definitions' + shape, so regenerating from a changed spec does not change what users program against. +- The generated definitions can live in their own namespace that the higher layers migrate onto, + and the existing implementation is retired. +- Orchestration and the high-level API are checked-in source, navigable and reviewable, and + regenerating the definitions never touches them. +- A binding that today combines the layers in one class (with injected orchestration and + enhancements) splits them: the generated definitions move to their own namespace, which the + orchestration and high-level API import. From 4b3f61d20fa068f61bb8d8b6514b65a1f585f1bf Mon Sep 17 00:00:00 2001 From: Titus Fortner Date: Sun, 21 Jun 2026 17:25:50 -0500 Subject: [PATCH 2/5] [docs] number BiDi ADR record by PR and link discussion --- ...d-protocol-layer.md => 17701-bidi-generated-protocol-layer.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename docs/decisions/{nnnnn-bidi-generated-protocol-layer.md => 17701-bidi-generated-protocol-layer.md} (100%) diff --git a/docs/decisions/nnnnn-bidi-generated-protocol-layer.md b/docs/decisions/17701-bidi-generated-protocol-layer.md similarity index 100% rename from docs/decisions/nnnnn-bidi-generated-protocol-layer.md rename to docs/decisions/17701-bidi-generated-protocol-layer.md From a49e5ab53fedcfe363bc9a75cc542f835c013744 Mon Sep 17 00:00:00 2001 From: Titus Fortner Date: Sun, 21 Jun 2026 17:28:31 -0500 Subject: [PATCH 3/5] [docs] set ADR title and discussion link to PR 17701 --- docs/decisions/17701-bidi-generated-protocol-layer.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/decisions/17701-bidi-generated-protocol-layer.md b/docs/decisions/17701-bidi-generated-protocol-layer.md index 147766e6621b3..055d233253914 100644 --- a/docs/decisions/17701-bidi-generated-protocol-layer.md +++ b/docs/decisions/17701-bidi-generated-protocol-layer.md @@ -1,7 +1,7 @@ -# NNNNN. BiDi's low-level definitions are generated from a shared spec model and imported, not injected +# 17701. BiDi's low-level definitions are generated from a shared spec model and imported, not injected - Status: Proposed -- Discussion: _PR pending_ +- Discussion: https://github.com/SeleniumHQ/selenium/pull/17701 ## Context From 67b42c92a62f58ee38d49bd84ce0447215b10ad0 Mon Sep 17 00:00:00 2001 From: Titus Fortner Date: Sun, 21 Jun 2026 18:11:52 -0500 Subject: [PATCH 4/5] [docs] retitle ADR: definitions generated without orchestration --- docs/decisions/17701-bidi-generated-protocol-layer.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/decisions/17701-bidi-generated-protocol-layer.md b/docs/decisions/17701-bidi-generated-protocol-layer.md index 055d233253914..29ffde9d28ee3 100644 --- a/docs/decisions/17701-bidi-generated-protocol-layer.md +++ b/docs/decisions/17701-bidi-generated-protocol-layer.md @@ -1,4 +1,4 @@ -# 17701. BiDi's low-level definitions are generated from a shared spec model and imported, not injected +# 17701. BiDi's low-level definitions are generated from a shared spec model without orchestration - Status: Proposed - Discussion: https://github.com/SeleniumHQ/selenium/pull/17701 From 6ce1f6e6db86d8fc6960c4c92cd1070c5743e245 Mon Sep 17 00:00:00 2001 From: Titus Fortner Date: Thu, 25 Jun 2026 11:14:24 -0500 Subject: [PATCH 5/5] [docs] clarify generated modules may call single commands; orchestration stays out --- .../17701-bidi-generated-protocol-layer.md | 52 +++++++++---------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/docs/decisions/17701-bidi-generated-protocol-layer.md b/docs/decisions/17701-bidi-generated-protocol-layer.md index 29ffde9d28ee3..0226c04e3f1de 100644 --- a/docs/decisions/17701-bidi-generated-protocol-layer.md +++ b/docs/decisions/17701-bidi-generated-protocol-layer.md @@ -10,13 +10,12 @@ A binding's BiDi support spans four layers, from the wire up: - **Transport / session substrate** — the connection, sending each command and correlating its response by the envelope id, and delivering inbound event frames upward. Domain-blind. -- **Low-level definitions** — the types, command shapes, and event shapes the spec defines, - including the id types (a subscription, an intercept) and the commands that produce and consume - them. -- **Orchestration** — the code that composes the definitions into capabilities and manages their - lifecycle: storing a subscription to unsubscribe later, mapping an intercept id to a handler, - matching an event to its registered callback, and wrapping events into the objects a handler - receives. +- **Low-level definitions** — the types, command shapes, and event shapes the spec defines + (including the id types — a subscription, an intercept — and the commands that produce and consume + them), exposed by generated modules that can execute a single command. +- **Orchestration** — the stateful coordination across those commands and events: storing a + subscription to unsubscribe later, mapping an intercept id to a handler, matching an event to its + registered callback, and wrapping events into the objects a handler receives. - **High-level API** — the protocol-neutral, idiomatic capabilities users program against. Bindings differ today in how they produce the definitions and how cleanly they separate them from @@ -46,17 +45,19 @@ defines the shape, the generated definitions need not match the existing impleme shape or byte-for-byte. The shared model is the one place the spec is interpreted and normalized, so bindings stay consistent with each other; each still emits its own language-idiomatic code from it. -**2. The generated definitions are read-only data, not the actionable surface.** Where a generated -object reaches a user — for example, the request object inside a network handler — it is immutable, -informational data; users act through the orchestration wrapper, not on the generated object. +**2. Generated data objects are immutable; generated modules may call single commands.** The +generated protocol data objects — command parameters and results, event payloads — are immutable +and information-only. Generated domain modules may expose directly callable single-command methods +(e.g. a generated `BrowsingContext.navigate(...)` that executes one command) as internal, unsupported +implementation APIs — not the supported, user-facing surface, which is the high-level API. -**3. Orchestration stays out of the low-level definitions.** The definitions carry the spec — types, -commands, and events — and nothing that coordinates them: subscription lifecycle, event dispatch, -mapping ids to handlers, and the objects handed to a handler live in a separate layer that imports -the definitions, not spliced into the generated classes (e.g. through an enhancements manifest). A -thin, stateless convenience over a single command is a lesser matter; what must not land here is -coordination. The definitions stay a projection of the spec, so regenerating them never disturbs the -layer that imports them, and they depend only on the transport's send-and-deliver interface. +**3. Stateful orchestration stays out of the generated layer.** Generated modules execute individual +commands, but own no stateful coordination: subscriptions, event dispatch, handler and intercept-id +mappings, body collectors, and lifecycle management live in the orchestration layer, which imports +the generated definitions rather than being spliced into them (e.g. through an enhancements manifest). +A thin convenience over a single command may be generated; what must not land here is coordination. +The generated definitions stay a projection of the spec, so regenerating them never disturbs the +orchestration that imports them, and they depend only on the transport's send-and-deliver interface. ## Considered options @@ -80,18 +81,17 @@ layer that imports them, and they depend only on the transport's send-and-delive inconsistencies forward. - Nothing supported depends on the generated definitions, so they need not match it. -- **Generate each binding independently from CDDL** — every binding interprets the spec on its own - (or hand-writes), with no shared model. - - Each binding normalizes types, names, and gap handling separately, so the bindings drift apart - over time — the cross-binding inconsistency this record exists to prevent. - - A shared, binding-neutral model avoids the drift while still letting each binding emit - language-idiomatic code from it: the source model is shared, the generated definitions are - per-language. +- **Generate each binding independently from CDDL** — every binding walks the CDDL/AST and builds + its own model with its own logic, rather than consuming one shared model. + - The grouping of modules, commands, and events follows the spec, but the normalization, naming, + and gap handling are re-derived per binding, so they drift apart over time. + - A shared, binding-neutral model makes those decisions once; each binding still emits + language-idiomatic code from it. ## Consequences -- The supported API depends only on the wrapper, not on the generated definitions' - shape, so regenerating from a changed spec does not change what users program against. +- What users program against is the high-level API, not the generated definitions, so regenerating + from a changed spec does not change it. - The generated definitions can live in their own namespace that the higher layers migrate onto, and the existing implementation is retired. - Orchestration and the high-level API are checked-in source, navigable and reviewable, and