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):

search-mount-v6.png

v7 (proposed):

search-mount-v7.png

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

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 main element? Broader scope catches other injection points (bare-/search default, 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-uid guard (R39) covers the case where a new query lands mid-injection.
Cook, John, Rishab Nithyanand, and Zubair Shafiq. 2020. “Inferring Tracker-Advertiser Relationships in the Online Advertising Ecosystem Using Header Bidding.” Proceedings on Privacy Enhancing Technologies (Popets) 2020 (1): 65–82. https://doi.org/10.2478/popets-2020-0005.
MDN Web Docs. n.d. “Intersection Observer Api.” https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API.
Roesner, Franziska, Tadayoshi Kohno, and David Wetherall. 2012. “Detecting and Defending against Third-Party Tracking on the Web.” In 9Th Usenix Symposium on Networked Systems Design and Implementation (Nsdi 12), 155–68. https://www.usenix.org/conference/nsdi12/technical-sessions/presentation/roesner.
WHATWG. n.d. “Html Living Standard: Link Types (Rel=Sponsored).” https://html.spec.whatwg.org/multipage/links.html#link-type-sponsored.