Search mount — spec (v7 draft, not landed)
Table of Contents
1. Purpose
Search-mount v7 is a dependency-inversion of the current
pocket-es integration. Today pocket-es calls into
window._sponsoredResearch.Format.interleave at render time —
pocket-es knows about sponsored. Under v7, pocket-es knows nothing;
the sponsored module attaches a MutationObserver to the results
container and injects sponsored slots after pocket-es renders.
Rationale: pocket-es is a search UI; interleaving sponsored inventory is not its concern. The direct call couples the two modules and forces every pocket-es change to consider sponsored regressions. The observer pattern gives sponsored a non-invasive attachment point.
Trade-off: the observer sees the rendered DOM, not the raw hit list. Injection is post-render, which means (1) a brief visible flash of organic-only results, and (2) a possible reflow when sponsored slots are injected. Placeholder-then-swap (already used in v6) mitigates both.
Status: spec captured, not landed. The v6 direct-call integration is what's shipping. v7 is a rewrite that becomes worthwhile when pocket-es changes at a rate that makes coordinated sponsored regressions expensive.
2. Requirements
- R37
- Per-query
page-uid. Every query starts a fresh observer run keyed by a UID. Prevents cross-query state bleed. - R38
- Clean teardown between queries. On new query: disconnect the observer, remove any injected sponsored DOM, clear the per-query event listeners.
- R39
- View-dedup. No duplicate impressions for the same
(page-uid, slot)pair on re-render — the observer will fire repeatedly during pocket-es's own render lifecycle, and each fire must be idempotent. - R40
- Non-permutation of organic. Invariant inherited from v6. Sponsored injection happens between organic DOM nodes but never reorders them.
3. Contract signature
Browser (proposed, sponsored_research/search_mount.cljs):
window._sponsoredResearch.Mount = {
install: (fn [selector] -> nil) ; wire the MutationObserver on <selector>
detach: (fn [] -> nil) ; disconnect + cleanup
page-uid: (fn [] -> string) ; current per-query UID
injected?: (fn [page-uid slot-idx] -> bool) ; view-dedup guard
}
Lifecycle:
new query
-> pocket-es dispatchEvent("wal:pocket-es-query", {q, uid})
-> Mount observer catches results-container mutation
-> Mount computes stride from organic count
-> Mount injects sponsored between organic (idempotent per (uid, slot))
-> Mount fires impression events via __wal_tracker
next query
-> pocket-es dispatchEvent("wal:pocket-es-query", {q, new-uid})
-> Mount.detach()
-> Mount.install(...) ;; new observer, new uid
4. Dependency graph — v6 vs v7
v6 (current):
v7 (proposed):
The v7 diagram is the point: pocket-es no longer knows about sponsored. The dashed edge marks the observer-mediated notification.
5. Related literature
- MutationObserver spec: WHATWG DOM Standard §4.3. https://dom.spec.whatwg.org/#mutation-observers.
- IntersectionObserver (MDN Web Docs, n.d.) is the
companion API — Mount uses MutationObserver to catch injection
time, and delegates viewport visibility to IntersectionObserver
via the tracker's
rescan. rel"sponsored"= (WHATWG, n.d.) — every injected slot carries this. Non-invasive attachment does not change the disclosure contract.- Cook et al. (Cook, Nithyanand, and Shafiq 2020) on the observability of third-party inclusion — the observer pattern is what third-party ad servers use to detect they've been mounted, so a first-party observer sits inside a well-studied topology.
- Roesner et al.'s taxonomy (Roesner, Kohno, and Wetherall 2012) classifies the observer pattern as "AA" (Analytics/Advertising with cross- frame access); the v7 mount is single-origin so it doesn't inherit the cross-frame concerns.
6. Cross-references
- pocket-es-integration/spec.org — v6 direct-call integration this spec inverts.
- sponsored-research/spec.org — supplies the sponsored inventory;
Mountwould attach to the samewindow._sponsoredResearch. - tracker/spec.org — Mount's post-inject step calls
__wal_tracker.rescan()to pick up new DOM. - beacon/spec.org — impressions from injected slots use the same envelope.
- ORACLE (proposed, not landed):
src/wal_sh/adtech/sponsored_research/search_mount.cljs - pocket-es — Specification — the search-side contract Mount observes from the outside.
7. Open questions
- Landing the rewrite. The v6 direct-call integration works. The cost of the rewrite is a coordinated deploy: both pocket-es and sponsored-research change on the same commit. Worth doing when pocket-es changes at a rate where the coupling is a live tax. Not now.
- Observer scope. Should Mount observe the results container only,
or the entire
mainelement? Broader scope catches other injection points (bare-/searchdefault, chumbox infinite feed) at the cost of more spurious mutation callbacks. - Race condition: pocket-es re-renders during Mount's injection
batch. Mutation-batching semantics say Mount sees a coherent
microtask; the
page-uidguard (R39) covers the case where a new query lands mid-injection.