Plugin / Docs / Advanced Patterns

Advanced Patterns — Build reliable flows

Beyond the basics: make flows that survive reloads, slow networks, flaky selectors, and user actions. Every snippet is safe to copy for beginners.

Canonical envelopes • Multi-tab via tabKey • v7.10.1

v7.10.1

1) Prerequisites

  • Read Detect the Extension (enable UI only after a successful ping).
  • Target domains must be in the Allow-list (extension popup).
  • Use the same minimal helper on all pages (below).
<script>
const ADMIN_ORIGIN='admin', EXT_ORIGIN='extension';
let _seq=0, _waiters=new Map();

addEventListener('message', (e) => {
  const d=e.data; if(!d||typeof d!=='object') return;
  if(d.origin!==EXT_ORIGIN) return;
  if(d.requestId && _waiters.has(d.requestId)){ _waiters.get(d.requestId)(d); _waiters.delete(d.requestId); }
});

function ask(action, args={}, timeoutMs=12000){
  const requestId='r'+(++_seq);
  postMessage({ origin:ADMIN_ORIGIN, type:'RTO_REQUEST', requestId, action, args }, '*');
  return new Promise((resolve,reject)=>{
    const t=setTimeout(()=>{ _waiters.delete(requestId); reject(Object.assign(new Error('timeout'),{code:'TIMEOUT'})); }, timeoutMs);
    _waiters.set(requestId, (resp)=>{ clearTimeout(t); resolve(resp); });
  });
}
</script>

2) Reliability Toolkit

2.1 Retry with backoff (copy-paste)

Great for transient errors (TIMEOUT, slow SPAs). Exponential delay prevents hammering.

async function askBackoff(action, args={}, tries=3, base=300){
  let n=0;
  while(true){
    try{
      const r = await ask(action, args, 12000);
      if (!r.ok) throw Object.assign(new Error(r.error?.message||'error'), r.error||{});
      return r;
    }catch(e){
      if (++n >= tries) throw e;
      await new Promise(r=>setTimeout(r, base * Math.pow(2, n-1)));
    }
  }
}
2.2 Wait-until (poll a condition)

Prefer explicit markers over fixed sleeps.

async function waitUntil(checkFn, timeoutMs=20000, step=500){
  const t0=Date.now();
  while (Date.now()-t0 < timeoutMs){
    if (await checkFn().catch(_=>false)) return true;
    await new Promise(r=>setTimeout(r, step));
  }
  throw Object.assign(new Error('TIMEOUT'), { code:'TIMEOUT' });
}
// Example: CSS class marker
await waitUntil(()=>
  ask('runJs', { code:'document.body.classList.contains("ready")' })
    .then(r=>!!(r.ok && r.data))
);
2.3 Circuit breaker (avoid repeated failures)

Stops hammering a page/API when failing repeatedly.

const Circuit={ fail:0, openUntil:0 };
async function guarded(step){
  const now=Date.now();
  if (Circuit.openUntil > now) throw new Error('CIRCUIT_OPEN');
  try{
    const out = await step(); Circuit.fail=0; return out;
  }catch(e){
    if (++Circuit.fail >= 3){ Circuit.openUntil = now + 15000; }
    throw e;
  }
}
// Usage
await guarded(()=>ask('navigate', { url:'https://app.example.com', focus:false }));

3) Guards & Assertions

Assert state → then act. This makes flows idempotent and easier to resume.

3.1 Ensure on the right page
async function ensureOn(prefix, tabKey){
  const u = await ask('getUrl', { tabKey }).catch(_=>null);
  if (!u?.ok || !u.data?.url?.startsWith(prefix)){
    await ask('navigate', { url: prefix, focus:false, tabKey });
  }
}
// Example
await ensureOn('https://app.example.com/dashboard', 'main');
3.2 Wait for a UI marker
const ok = await ask('runJs', { code:'!!document.querySelector(".badge.ready")', tabKey:'main' });
if (!(ok.ok && ok.data)){
  await askBackoff('waitFor', { selector:'.badge.ready', timeoutMs:30000, tabKey:'main' }, 3, 400);
}

4) Idempotent Actions

Running your script twice should not break the page. Check first, then change.

4.1 Safe checkbox/radio
await ask('runJs', { code:`(function(){
  const el=document.querySelector('#tos'); if(!el) return false;
  if(!el.checked){ el.checked=true; el.dispatchEvent(new Event('change',{bubbles:true})); }
  return true;
})()`, tabKey:'main' });
4.2 Set <select> if not already selected
await ask('runJs', { code:`(function(){
  const s=document.querySelector('select#status'); if(!s) return false;
  if (s.value!=='active'){ s.value='active'; s.dispatchEvent(new Event('change',{bubbles:true})); }
  return true;
})()`, tabKey:'main' });

5) SPA Playbook (slow renders)

  • Prefer waitFor and explicit “ready” markers over fixed delays.
  • Probe table row counts or route changes with small runJs snippets.
  • Use backoff when waiting for expensive components.
5.1 Table ready probe (rows > 0)
await waitUntil(()=>
  ask('runJs', { code:`(function(){
    const t=document.querySelector('table.data');
    return !!(t && t.tBodies[0] && t.tBodies[0].rows.length);
  })()`, tabKey:'main' }).then(r=>!!(r.ok && r.data)),
  30000, 600
);

6) Selector Strategy (reduce flakes)

  • Prefer stable attributes like [data-testid="…"] or long-lived IDs.
  • Avoid deep CSS chains that break when markup shifts.
  • Precede actions with waitFor to ensure the element exists and is visible.
6.1 Example: robust selectors
// Good
await ask('domClick', { selector:'[data-testid="submit"]', tabKey:'main' });
// Risky (brittle)
await ask('domClick', { selector:'.form > div:nth-child(3) button.btn-primary', tabKey:'main' });

7) Shadow DOM

Traverse shadowRoot explicitly. Cross-origin iframes are still off-limits by design.

7.1 Write into a shadow input
await ask('runJs', { code:`(function(){
  const host=document.querySelector('my-input'); if(!host||!host.shadowRoot) return false;
  const input=host.shadowRoot.querySelector('input'); if(!input) return false;
  input.value='hello'; input.dispatchEvent(new Event('input',{bubbles:true}));
  return true;
})()`, tabKey:'main' });

8) Multi-tab Focus

  • Attach a logical key per flow: tabKey:'main', 'auth', etc.
  • Use focus only when you need the user to see/confirm.
  • focusByPrefix helps adopt an already-open tab.
await ask('focus', { tabKey:'main' });
// Or attach by URL prefix:
await ask('focusByPrefix', { prefix:'https://docs.example.com/' });

9) Safe runJs Checklist

  • Keep snippets short; wrap in an IIFE; always return a value.
  • Guard for null (if (!el) return false;).
  • Expect CSP to block some constructs; failures show as EXECUTION_ERROR.
await ask('runJs', { code:`(function(){
  const el=document.querySelector('#status'); if(!el) return false;
  el.textContent='Connected ✅'; return true;
})()`, tabKey:'main' });

10) Utilities Mini-Library (drop-in)

Paste once in your admin layout; wraps common patterns (v7.10.1 actions).

const RTO = {
  open: (url,opts={}) => ask('open', { url, ...opts }),
  navigate: (url,opts={}) => ask('navigate', { url, ...opts }),
  focus: (opts={}) => ask('focus', opts),
  focusByPrefix: (prefix) => ask('focusByPrefix', { prefix }),
  getUrl: (opts={}) => ask('getUrl', opts),
  getTitle: (opts={}) => ask('getTitle', opts),

  waitFor: (selector, timeoutMs=15000, opts={}) => ask('waitFor', { selector, timeoutMs, ...opts }),
  domType: (selector, value, opts={}) => ask('domType', { selector, value, ...opts }),
  domClick: (selector, opts={}) => ask('domClick', { selector, ...opts }),
  selectSetValue: (selector, value, opts={}) => ask('selectSetValue', { selector, value, ...opts }),
  submit: (selector, opts={}) => ask('submit', { selector, ...opts }),
  runJs: (code, opts={}) => ask('runJs', { code, ...opts }),
};

Aliases supported in recent builds: domSetValuedomType, submitFormsubmit. Prefer the canonical names above to keep docs and examples consistent.

11) End-to-End Examples

11.1 Login → Ready badge → Title assert
await ask('open', { url:'https://app.example.com/login', focus:false, newTab:true, tabKey:'auth' });
await ask('waitFor', { selector:'#email', timeoutMs:25000, tabKey:'auth' });
await ask('domType', { selector:'#email', value:'user@example.com', tabKey:'auth' });
await ask('domType', { selector:'#note', value:'hello world', tabKey:'auth' });
await ask('domClick', { selector:'button[type=submit]', tabKey:'auth' });
await ask('waitFor', { selector:'.badge.ready', timeoutMs:30000, tabKey:'auth' });
const title = await ask('getTitle', { tabKey:'auth' });
if (!(title.ok && /Dashboard/i.test(title.data.title||''))) throw new Error('Unexpected page');
11.2 Playlist with resilient waits
const pages=['/a','/b','/c'].map(x=>'https://example.com'+x);
for (const url of pages){
  await askBackoff('navigate', { url, focus:false, tabKey:'qa' }, 4, 250);
  await askBackoff('waitFor', { selector:'body', timeoutMs:12000, tabKey:'qa' }, 3, 300);
  const h1 = await ask('runJs', { code:'(document.querySelector("h1")||{}).textContent||""', tabKey:'qa' });
  console.log('Visited:', url, 'Title:', (h1.ok?h1.data:''));
  await new Promise(r=>setTimeout(r, 600)); // polite throttle
}
Need help writing safe flows?
Try the ChatGPT helper: Remote Tab Opener Copilot.

Next Pages

Page Description
Start Overview, how it works, setup, quick start, and links to all sections.
Detect the Extension Detection contract (ping/pong), install prompts, and graceful fallbacks when the extension isn’t present.
Allow-list & Permissions Deny-by-default model, domain allow-list via popup, and suggested UX to guide users to enable hosts.
LAN / LocalhostHow to safely enable intranet/localhost (double barrier + allow-list).
Remote Tab Control Open/navigate/focus the controlled tab, read URL/title, select an existing tab, lifecycle & restore patterns.
DOM & Automation Action catalog (domType, domClick, selectSetValue, submit, waitFor) and safe interaction tips.
Recipes & Flows Copy-paste flows: resilient login, dashboard checks, table waits, filters, playlists, and allow-list banners.
Events Event stream reference, subscription helpers, live logger widget, and basic metrics/durations collection.
Diagnostics Inline console, error cookbook, QA self-check, and performance tips for troubleshooting flows quickly.
Full API Reference Canonical message envelopes, request/response schemas, error codes, and return shapes for every action.
Compatibility Supported browsers/versions, permissions nuances, CSP/iframe notes, and known limitations or workarounds.
Advanced Patterns Idempotent flows, retries/backoff, state restore after reload, playlist strategies, and robust selectors.
Favorites Star and group URLs, quick actions (open/focus/navigate), export/import sets, and playlist runs from favorites.
Real-World Flows (E2E) Run end-to-end flows from your admin page: helper setup, modern message contract, copy-paste journeys (login, multi-tab, extract), lightweight assertions, best practices, and troubleshooting.