PlacementController
Framework-agnostic placement lifecycle controller.
Encapsulates the same register → decide → impression-track → interact flow
that the React usePlacement() hook provides, but without any framework dependency.
Subscribe to state changes with onChange and read the current state
with state, or just await load() for a one-shot pattern.
Example
Section titled “Example”const ctrl = new PlacementController(sdk, { surfaceSlot: { id: 'upsell_banner' } });ctrl.onChange(() => updateUI(ctrl.state));await ctrl.load();Accessors
Section titled “Accessors”content
Section titled “content”Get Signature
Section titled “Get Signature”get content():
RevTurbineDecisionContent|null
Current resolved content, or null if no decision has been loaded.
Returns
Section titled “Returns”RevTurbineDecisionContent | null
decision
Section titled “decision”Get Signature
Section titled “Get Signature”get decision():
RevTurbinePlacementDecision|null
Current decision, or null if not yet loaded.
Returns
Section titled “Returns”RevTurbinePlacementDecision | null
placementId
Section titled “placementId”Get Signature
Section titled “Get Signature”get placementId():
string
Registered placement ID, or empty string before first load().
Returns
Section titled “Returns”string
Get Signature
Section titled “Get Signature”get state():
PlacementControllerState
Current state snapshot.
Returns
Section titled “Returns”visible
Section titled “visible”Get Signature
Section titled “Get Signature”get visible():
boolean
Convenience: true when the current decision says the placement is visible.
Returns
Section titled “Returns”boolean
Methods
Section titled “Methods”ctaClick()
Section titled “ctaClick()”ctaClick(
ctaTarget?):Promise<void>
Record a CTA click interaction.
Parameters
Section titled “Parameters”ctaTarget?
Section titled “ctaTarget?”string
Returns
Section titled “Returns”Promise<void>
ctaComplete()
Section titled “ctaComplete()”ctaComplete(
ctaTarget?):Promise<void>
Record a CTA completion interaction and hide the placement.
Parameters
Section titled “Parameters”ctaTarget?
Section titled “ctaTarget?”string
Returns
Section titled “Returns”Promise<void>
dismiss()
Section titled “dismiss()”dismiss(
cooldownMs?):Promise<void>
Record a dismissal.
Parameters
Section titled “Parameters”cooldownMs?
Section titled “cooldownMs?”number
Optional explicit window. Omit it — which is the normal
case — and the SDK resolves the payload’s authored caps.cooldown_days,
falling back to the 7-day default. This used to default to 24h here and
pass it ALWAYS, so the authored value and the 7-day default were both
unreachable from React (plan 233 TASK-8b).
Returns
Section titled “Returns”Promise<void>
dispose()
Section titled “dispose()”dispose():
void
Stop listening for Playbook loads and user-context changes. Idempotent; call
it when the owning component unmounts (usePlacement does).
Returns
Section titled “Returns”void
load()
Section titled “load()”load():
Promise<RevTurbinePlacementDecision|null>
Register the placement (if needed) and fetch a decision.
When autoTrackImpression is true (default), a visible decision
automatically records an impression event.
Returns
Section titled “Returns”Promise<RevTurbinePlacementDecision | null>
markRendered()
Section titled “markRendered()”markRendered():
void
Called when the placement’s visual root renders (plan 144 TASK-11). Emits
placement_rendered once, and under placementExposure: 'render' credits
the presentation now. Idempotent.
Returns
Section titled “Returns”void
markVisible()
Section titled “markVisible()”markVisible(
basis?):void
Called by the viewport-exposure substrate when the placement’s visual root
first enters the viewport — or immediately with 'render_fallback' when
IntersectionObserver is unavailable (plan 144 TASK-9/11 / AC-9, AC-10).
Emits placement_exposed once with the basis, and under
placementExposure: 'viewport' a viewport exposure credits the presentation
here (Q-8). Idempotent.
Parameters
Section titled “Parameters”basis?
Section titled “basis?”ExposureBasis = 'viewport'
how exposure was established; defaults to 'viewport'
Returns
Section titled “Returns”void
onChange()
Section titled “onChange()”onChange(
listener): () =>void
Subscribe to state changes. Returns an unsubscribe function.
Parameters
Section titled “Parameters”listener
Section titled “listener”Returns
Section titled “Returns”() => void
Example
Section titled “Example”const unsub = ctrl.onChange(() => console.log(ctrl.state));// later...unsub();refresh()
Section titled “refresh()”refresh():
Promise<RevTurbinePlacementDecision|null>
Re-fetch the placement decision (clears impression + render + exposure tracking).
Returns
Section titled “Returns”Promise<RevTurbinePlacementDecision | null>
remindMeLater()
Section titled “remindMeLater()”remindMeLater(
seconds?):Promise<void>
Alias for snooze.
Parameters
Section titled “Parameters”seconds?
Section titled “seconds?”number
Returns
Section titled “Returns”Promise<void>
reset()
Section titled “reset()”reset():
void
Reset the controller state (clears decision, placement ID, etc.).
Returns
Section titled “Returns”void
snooze()
Section titled “snooze()”snooze(
seconds?):Promise<void>
Record a snooze / remind-me-later.
Parameters
Section titled “Parameters”seconds?
Section titled “seconds?”number
Optional explicit window. Omit it and the SDK resolves the
payload’s authored remind_later_minutes, falling back to the tenant
default (plan 233 TASK-8c). Remind-later is a DIFFERENT window from
dismiss and resolves independently.
Returns
Section titled “Returns”Promise<void>
watchUserContext()
Section titled “watchUserContext()”watchUserContext(): () =>
void
Re-decide whenever the user context changes (BL-0004).
The counterpart to EntitlementGate.watchUserContext, and the reason
sdk.convert() now removes a converted placement in place: conversion writes
no suppression — eligibility is a function of the user’s CURRENT plan and
targeting — so the only thing that can take a converted upgrade banner off
screen is a re-decide against the reloaded context. Without this the
placement stayed mounted until something else happened to re-resolve it, and
the host had to call refresh() itself or use the component’s
ctaComplete() (the limitation plan 236 TASK-13 recorded).
Only re-decides a controller that has already resolved something: before the
first load() there is nothing on screen to be stale, and firing then would
turn every startup identify() into a duplicate decision.
Calls load() rather than refresh() on purpose — refresh() clears
impression/exposure tracking, so a context change would re-credit a
presentation the user never saw again.
Returns an unsubscribe function; dispose also drops it.
Returns
Section titled “Returns”() => void