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
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
waitForand explicit “ready” markers over fixed delays. - Probe table row counts or route changes with small
runJssnippets. - 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
waitForto 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
focusonly when you need the user to see/confirm. focusByPrefixhelps 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: domSetValue → domType, submitForm → submit. 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
}
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 / Localhost | How 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. |
Need help writing safe flows?