Follow-up to #380 / #692.
Gap
PgBuyerAgentRegistry.add_mutation_observer() ships in #692 but the registry is append-only and unlocked:
self._mutation_observers: list[MutationObserver] = []
def add_mutation_observer(self, observer):
self._mutation_observers.append(observer)
Two consequences:
- No removal. Tests that wire fixtures across reused registry instances (uncommon in production, common in test suites) accumulate observers across the suite. A test that asserts "observer fires once" will leak into the next test that asserts "observer fires once."
- Append race with iteration.
_notify_mutation iterates self._mutation_observers on the mutation hot path. A late-registered observer added concurrently with a commit can miss its first mutation. In practice all wiring happens at boot, so the window is theoretical — but the lack of contract is worth documenting either way.
Proposed
def add_mutation_observer(self, observer: MutationObserver) -> None: ...
def remove_mutation_observer(self, observer: MutationObserver) -> bool:
\"\"\"Returns True if observer was registered and removed.\"\"\"
...
Either:
- (a) document that observers must be wired before any mutation traffic and the registry is append-only at that point — accept the test-fixture footgun, OR
- (b) guard `_mutation_observers` with a lock and provide `remove_mutation_observer`.
The cache-invalidation use case from #692 only ever needs one observer for the lifetime of the registry, so (a) is defensible. But adopters building observability hooks (audit emit, metrics counter) will want to wire and unwire per request scope — those will need (b).
Why now
Closing the API surface in a follow-up is cheaper than adding remove later when adopters have already started subclassing or wrapping the registry.
Acceptance
remove_mutation_observer API + conformance test, OR explicit documented constraint that observers are register-only at boot
- No regression on existing
add_mutation_observer semantics
Follow-up to #380 / #692.
Gap
PgBuyerAgentRegistry.add_mutation_observer()ships in #692 but the registry is append-only and unlocked:Two consequences:
_notify_mutationiteratesself._mutation_observerson the mutation hot path. A late-registered observer added concurrently with a commit can miss its first mutation. In practice all wiring happens at boot, so the window is theoretical — but the lack of contract is worth documenting either way.Proposed
Either:
The cache-invalidation use case from #692 only ever needs one observer for the lifetime of the registry, so (a) is defensible. But adopters building observability hooks (audit emit, metrics counter) will want to wire and unwire per request scope — those will need (b).
Why now
Closing the API surface in a follow-up is cheaper than adding
removelater when adopters have already started subclassing or wrapping the registry.Acceptance
remove_mutation_observerAPI + conformance test, OR explicit documented constraint that observers are register-only at bootadd_mutation_observersemantics