Add scoped native history and window agent controls

This commit is contained in:
Karl committed 2026-10-13 18:48:40 +02:00
1 parent adf0940e5f
commit 61fcd8336f
27 files changed
+503 -25

No files matched your search

+7 -4
View File
@@ -1,14 +1,17 @@
'use strict';
// Native history only: pages and agents cannot supply a navigation target.
async function performHistory({contents,action,allowed,current,signal,timeoutMs=10000}={}) {
if(!contents||!['back','forward','reload'].includes(action)||typeof allowed!=='function'||typeof current!=='function'||!Number.isFinite(timeoutMs)||timeoutMs<1||timeoutMs>60000)throw Error('Invalid history operation');
async function performHistory({contents,action,allowed,current,signal,index,getSnapshot,historyBinding,timeoutMs=10000}={}) {
if(!contents||!['back','forward','reload','index'].includes(action)||typeof allowed!=='function'||typeof current!=='function'||!Number.isFinite(timeoutMs)||timeoutMs<1||timeoutMs>60000||action==='index'&&(!Number.isSafeInteger(index)||index<0||typeof getSnapshot!=='function'||typeof historyBinding!=='string'))throw Error('Invalid history operation');
const guard=async(phase='before')=>{if(signal?.aborted)throw Error('History operation cancelled');if(contents.isDestroyed?.())throw Error('History contents unavailable');if(await current(phase)!==true)throw Error('History context changed');if(signal?.aborted)throw Error('History operation cancelled');};
const target=()=>action==='reload'?contents.getURL():contents.navigationHistory.getEntryAtIndex(contents.navigationHistory.getActiveIndex()+(action==='back'?-1:1))?.url;
const target=()=>action==='reload'?contents.getURL():contents.navigationHistory.getEntryAtIndex(action==='index'?index:contents.navigationHistory.getActiveIndex()+(action==='back'?-1:1))?.url;
await guard();const url=target();
if(!url)return {completed:false,url:contents.getURL()};
let parsed;try{parsed=new URL(url);}catch{throw Error('Invalid history destination');}
if(!['http:','https:'].includes(parsed.protocol)||parsed.username||parsed.password||!await allowed(url))throw Error('History destination denied');
await guard();if(target()!==url)throw Error('History destination changed');
if(action==='index'&&index===contents.navigationHistory.getActiveIndex()){
if(JSON.stringify(getSnapshot())!==historyBinding||contents.getURL()!==url)throw Error('History snapshot changed');return {completed:true,url};
}
return new Promise((resolve,reject)=>{
let settled=false,dispatched=false,checking=false;
const cleanup=()=>{clearTimeout(timer);signal?.removeEventListener('abort',abort);for(const [name,fn] of listeners)contents.removeListener(name,fn);};
@@ -21,7 +24,7 @@ async function performHistory({contents,action,allowed,current,signal,timeoutMs=
const timer=setTimeout(()=>fail(Error('History operation timed out')),timeoutMs);
for(const [name,fn] of listeners)contents.on(name,fn);signal?.addEventListener('abort',abort,{once:true});
if(signal?.aborted){abort();return;}
try{dispatched=true;if(action==='reload')contents.reload();else contents.navigationHistory.goToIndex(contents.navigationHistory.getActiveIndex()+(action==='back'?-1:1));}catch(error){fail(error);}
try{if(action==='index'&&JSON.stringify(getSnapshot())!==historyBinding)throw Error('History snapshot changed');if(target()!==url)throw Error('History destination changed');dispatched=true;if(action==='reload')contents.reload();else contents.navigationHistory.goToIndex(action==='index'?index:contents.navigationHistory.getActiveIndex()+(action==='back'?-1:1));}catch(error){fail(error);}
});
}
module.exports={performHistory};