31 lines
3.7 KiB
JavaScript
31 lines
3.7 KiB
JavaScript
'use strict';
|
|
// Native history only: pages and agents cannot supply a navigation target.
|
|
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(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);};
|
|
const fail=error=>{if(settled)return;settled=true;cleanup();if(dispatched&&!contents.isDestroyed?.())try{contents.stop();}catch{}reject(error);};
|
|
const complete=async()=>{if(!dispatched||settled||checking)return;checking=true;try{await guard('after');if(contents.getURL()!==url)throw Error('History destination changed');if(!await allowed(url))throw Error('History destination denied');await guard('after');if(contents.getURL()!==url)throw Error('History destination changed');if(settled)return;settled=true;cleanup();resolve({completed:true,url});}catch(error){fail(error);}finally{checking=false;}};
|
|
const inPage=(_event,eventUrl,isMainFrame)=>{if(isMainFrame===true){if(eventUrl!==url)fail(Error('History destination changed'));else void complete();}};
|
|
const loadFailed=(_event,_code,_description,_url,isMainFrame)=>{if(isMainFrame===true)fail(Error('History navigation failed'));};
|
|
const abort=()=>fail(Error('History operation cancelled'));
|
|
const listeners=[['did-finish-load',complete],['did-navigate-in-page',inPage],['did-fail-load',loadFailed],['destroyed',()=>fail(Error('History contents unavailable'))]];
|
|
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{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};
|