Skip to content

Fire OFFLOADER_PAIRING_ADDED on request_pair for cross-tab sync#1211

Draft
esphbot wants to merge 3 commits into
esphome:mainfrom
esphbot:koan/offloader-pairing-added-event
Draft

Fire OFFLOADER_PAIRING_ADDED on request_pair for cross-tab sync#1211
esphbot wants to merge 3 commits into
esphome:mainfrom
esphbot:koan/offloader-pairing-added-event

Conversation

@esphbot
Copy link
Copy Markdown
Contributor

@esphbot esphbot commented Jun 5, 2026

What

Fire a new OFFLOADER_PAIRING_ADDED bus event when request_pair creates an offloader pairing, so connected clients that didn't issue the command build the row from the event.

Why

request_pair persisted the StoredPairing and returned a PairingSummary in the command response, but fired no bus event for the creation. The originating tab seeds its own row from that response (see the frontend onPairRequestSent), but a second connected tab gets nothing — and the later OFFLOADER_PAIR_STATUS_CHANGED only flips an already-known row (patchOffloadPairing no-ops on a missing key). So a second tab's pairings list stayed stale until a full reload. This is the snapshot-plus-events cross-tab desync the architecture explicitly avoids (same vein as #1188 / #1189).

How

  • New EventType.OFFLOADER_PAIRING_ADDED + OffloaderPairingAddedData TypedDict carrying every PairingSummary field, so a subscriber builds the row from the event alone — mirroring the receiver-side REMOTE_BUILD_PAIR_REQUEST_RECEIVED.
  • Fired from request_pair for both the PENDING and the re-pair-short-circuit APPROVED branches, off the single-source-of-truth _pairing_summary_for projection.
  • Needs the matching frontend handler (separate PR against device-builder-frontend) to insert the row.

Testing

  • New test_controller_request_pair_fires_pairing_added_event (end-to-end through the WS-command shell) asserts the event fires once with the full payload.
  • pytest tests/test_remote_build_peer_link_client.py tests/test_remote_build_controller.py tests/test_subscribe_events_cleanup.py tests/models → 426 passed. ruff + mypy clean.

Types of changes

  • Bugfix (non-breaking change which fixes an issue) — bugfix

Frontend coordination


Quality Report

Changes: 7 files changed, 106 insertions(+), 3 deletions(-)

Code scan: clean

Tests: failed ([Errno 13] Permission denied: 'pytest')

Branch hygiene: clean

Generated by Kōan

@codspeed-hq
Copy link
Copy Markdown

codspeed-hq Bot commented Jun 5, 2026

Merging this PR will not alter performance

✅ 27 untouched benchmarks
⏩ 1 skipped benchmark1


Comparing esphbot:koan/offloader-pairing-added-event (b234da7) with main (9e136f2)

Open in CodSpeed

Footnotes

  1. 1 benchmark was skipped, so the baseline result was used instead. If it was deleted from the codebase, click here and archive it to remove it from the performance reports.

@codecov-commenter
Copy link
Copy Markdown

codecov-commenter commented Jun 5, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 99.42%. Comparing base (9e136f2) to head (b234da7).

Additional details and impacted files

Impacted file tree graph

@@           Coverage Diff           @@
##             main    #1211   +/-   ##
=======================================
  Coverage   99.42%   99.42%           
=======================================
  Files         217      217           
  Lines       16140    16160   +20     
=======================================
+ Hits        16047    16067   +20     
  Misses         93       93           
Flag Coverage Δ
py3.12 99.39% <100.00%> (+<0.01%) ⬆️
py3.14 99.42% <100.00%> (+<0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
...vice_builder/controllers/remote_build/offloader.py 100.00% <100.00%> (ø)
..._builder/controllers/remote_build/pair_commands.py 100.00% <100.00%> (ø)
...ce_builder/controllers/remote_build/pair_status.py 100.00% <100.00%> (ø)
esphome_device_builder/models/common.py 100.00% <100.00%> (ø)
...ome_device_builder/models/remote_build/__init__.py 100.00% <ø> (ø)
...ce_builder/models/remote_build/offloader_events.py 100.00% <100.00%> (ø)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@esphbot
Copy link
Copy Markdown
Contributor Author

esphbot commented Jun 5, 2026

Frontend half (consumes this event): esphome/device-builder-frontend#637

@github-actions github-actions Bot added the bugfix Bug fix label Jun 5, 2026
@bdraco bdraco requested a review from Copilot June 5, 2026 03:23
@bdraco
Copy link
Copy Markdown
Member

bdraco commented Jun 5, 2026

Looks spot on . real gap

@bdraco
Copy link
Copy Markdown
Member

bdraco commented Jun 5, 2026

@esphbot rr

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds an offloader-side bus event to keep pairing lists consistent across multiple connected clients (e.g., multiple browser tabs) by emitting a “row created” event when request_pair creates/recreates a pairing.

Changes:

  • Introduces EventType.OFFLOADER_PAIRING_ADDED and its OffloaderPairingAddedData payload.
  • Fires OFFLOADER_PAIRING_ADDED from request_pair for both PENDING and short-circuit APPROVED outcomes.
  • Adds an end-to-end test asserting the event is emitted from the offloader pairing flow.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
tests/test_remote_build_peer_link_client.py Adds a new test ensuring request_pair emits the pairing-added event.
esphome_device_builder/models/remote_build/offloader_events.py Defines the new OffloaderPairingAddedData TypedDict payload.
esphome_device_builder/models/remote_build/__init__.py Re-exports OffloaderPairingAddedData.
esphome_device_builder/models/common.py Adds EventType.OFFLOADER_PAIRING_ADDED.
esphome_device_builder/controllers/remote_build/pair_status.py Adds a helper to build/fire the OFFLOADER_PAIRING_ADDED payload.
esphome_device_builder/controllers/remote_build/pair_commands.py Fires the new event from request_pair using the existing _pairing_summary_for projection.
esphome_device_builder/controllers/remote_build/offloader.py Adds a thin controller method delegating to the new fire helper.

Comment on lines +842 to +862
await offloader.request_pair(
hostname="127.0.0.1",
port=server.port,
pin_sha256=expected_pin,
receiver_label="my-receiver",
offloader_label="my-builder",
)

added = [
call.args[1]
for call in offloader._db.bus.fire.call_args_list
if call.args[0] is EventType.OFFLOADER_PAIRING_ADDED
]
assert len(added) == 1
payload = added[0]
assert payload["pin_sha256"] == expected_pin
assert payload["receiver_hostname"] == "127.0.0.1"
assert payload["receiver_port"] == server.port
assert payload["label"] == "my-receiver"
assert payload["status"] == "pending"
assert payload["enabled"] is True
Comment on lines +35 to +50
class OffloaderPairingAddedData(TypedDict):
"""
Payload for ``EventType.OFFLOADER_PAIRING_ADDED``.

Fires from ``request_pair`` when a :class:`StoredPairing` row
is created (or re-created on a re-pair). ``OFFLOADER_PAIR_STATUS_CHANGED``
only marks status *flips* of an already-known row, so without
this event a second connected tab never learns of a pairing
the originating tab just made (it seeds its own row from the
command response) — cross-tab desync until reload.

Carries every :class:`PairingSummary` field so a subscriber
builds the row from the event alone. ``status`` is
``"pending"`` for a fresh request or ``"approved"`` when the
receiver short-circuited the inbox dance on a re-pair.
"""
@esphbot
Copy link
Copy Markdown
Contributor Author

esphbot commented Jun 5, 2026

PR Review — Fire OFFLOADER_PAIRING_ADDED on request_pair for cross-tab sync

Solid, well-scoped fix for a real cross-tab desync gap. Merge-ready; one docstring nit.

  • Closes a documented snapshot-plus-events failure mode (CREATE fired no event, only later STATUS_CHANGED flips); maintainer confirmed "real gap".
  • Follows established patterns: TypedDict payload carrying full row, fire helper next to fire_offloader_pair_status_changed, thin controller delegate, __init__ re-export.
  • request_pair if/else refactor preserves both spawn paths and fires once for PENDING and re-pair-APPROVED — correct.
  • 100% coverage on changed lines; test asserts the event fires once with key fields.
  • Only nit: the new TypedDict docstring carries motivation the repo style guide asks to keep out of docstrings.

🟢 Suggestions

1. TypedDict docstring carries motivation CLAUDE.md asks to omit (`esphome_device_builder/models/remote_build/offloader_events.py`, L35-53)

Per the repo's code-style rule (CLAUDE.md: "Never put in docstrings or comments: rationale / motivation"), this docstring leans heavily into why — the cross-tab desync narrative, what a second tab does, the seed-from-command-response detail. That belongs in the PR body (where it already is), not the payload contract.

Trim to the contract: what the event signals and what status means.

class OffloaderPairingAddedData(TypedDict):
    """
    Payload for ``EventType.OFFLOADER_PAIRING_ADDED`` (a row created by
    ``request_pair``). Carries every ``PairingSummary`` field so a
    subscriber builds the row from the event alone. ``status`` is
    ``"pending"`` for a fresh request, ``"approved"`` on a re-pair
    short-circuit.
    """

Matches the Copilot reviewer's note on the same lines. Non-blocking.


Automated review by Kōan (Claude) HEAD=413a6db

bdraco added 2 commits June 5, 2026 03:42
request_pair created a StoredPairing row but fired no bus event, so a
second connected tab never learned of a pairing the originating tab made
(it seeds its own row from the command response). The later
OFFLOADER_PAIR_STATUS_CHANGED only flips an already-known row, so the
second tab's row stayed absent until a full reload.

Add EventType.OFFLOADER_PAIRING_ADDED + OffloaderPairingAddedData carrying
the full PairingSummary, fired from request_pair for both the PENDING and
re-pair-APPROVED branches, mirroring the receiver-side
REMOTE_BUILD_PAIR_REQUEST_RECEIVED row-create event.
@esphbot
Copy link
Copy Markdown
Contributor Author

esphbot commented Jun 5, 2026

Rebase with requested adjustments

Branch koan/offloader-pairing-added-event was rebased onto main and review feedback was applied.

Changes applied

  • Both review comments addressed.
  • Test (test_remote_build_peer_link_client.py:862) — Capture returned PairingSummary and assert event payload == summary.to_dict() instead of per-field subset. Pins the full-row contract and the single-source-of-truth projection, per @Copilot.
  • Docstring (offloader_events.py:50) — Trimmed OffloaderPairingAddedData docstring to the contract (event meaning + status semantics), dropping the cross-tab-desync motivation, per @Copilot and CLAUDE.md docstring rule.

Stats

7 files changed, 92 insertions(+), 3 deletions(-)
Actions performed
  • Already-solved check: negative (confidence=high, reasoning=main's request_pair returns the PairingSummary but fires no OFFLOADER_PAIRING_ADDED event, and no su)
  • Rebased koan/offloader-pairing-added-event onto upstream/main
  • Applied review feedback
  • Pre-push CI check: previous run #26990668926 failed
  • Pre-push CI fix: no changes needed or Claude found nothing to fix
  • Force-pushed koan/offloader-pairing-added-event to origin
  • CI check enqueued in ## CI (async)

CI status

CI will be checked asynchronously.


Automated by Kōan

@esphbot esphbot force-pushed the koan/offloader-pairing-added-event branch from 413a6db to 5fd418f Compare June 5, 2026 03:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bugfix Bug fix

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants