import { collectPropertyNames , stringReplace , isFunctionProperty , loadNextScript , safeCall , hasOwn , noop , getTargetUrl , loadNextLink , unique } from './util' ; import { retryTimesProp , maxRetryCountProp , onRetryProp , domainProp , innerProxyProp , innerOnloadProp , innerOnerrorProp , linkTag , scriptTag , hookedIdentifier , ignoreIdentifier , doc , ScriptElementCtor , LinkElementCtor } from './constants' ; import { retryCollector } from './collector' ; import { prepareDomainMap , extractInfoFromUrl } from './url' ; import { InnerAssetsRetryOptions } from './assets-retry' ; type DynamicElement = HTMLScriptElement | HTMLLinkElement ; export interface HookedElement { [ innerProxyProp ] : DynamicElement ; [ innerOnerrorProp ] : ( e : Partial < Event > ) => void ; [ x ] : any ; } let scriptAndLinkProperties ; try { scriptAndLinkProperties = unique ( [ ... collectPropertyNames ( ScriptElementCtor . prototype ) , ... collectPropertyNames ( LinkElementCtor . prototype ) ] ) ; } catch ( _ ) { } param { any } self param { object } opts returns const getHookedElementDescriptors = function ( self , opts ) { const maxRetryCount = opts [ maxRetryCountProp ] ; const domainMap = prepareDomainMap ( opts [ domainProp ] ) ; const onRetry = opts [ onRetryProp ] ; return scriptAndLinkProperties . reduce ( function ( descriptor , key ) { const isFn = isFunctionProperty ( ScriptElementCtor . prototype , key ) ; if ( isFn ) { descriptor [ key ] = { value : function ( ) { return ( self [ innerProxyProp ] as any ) [ key ] . apply ( self [ innerProxyProp ] , arguments ) ; } } ; } else { descriptor [ key ] = { set : function ( newVal ) { const realElement = self [ innerProxyProp ] ; if ( key === 'onerror' ) { self [ innerOnerrorProp ] = newVal ; realElement . onerror = function ( event ) { if ( typeof event === 'string' ) return ; event . stopPropagation && event . stopPropagation ( ) ; const callOriginalOnError = ( ) => safeCall ( self [ innerOnerrorProp ] , realElement , event ) ; const url = getTargetUrl ( realElement ) ; const [ currentDomain , currentCollector ] = extractInfoFromUrl ( url , domainMap ) ; const shouldIgnore = realElement . hasAttribute ( ignoreIdentifier ) ; if ( ! currentDomain || ! currentCollector || shouldIgnore ) { return callOriginalOnError ( ) ; } const newSrc = stringReplace ( url , currentDomain , domainMap [ currentDomain ] ) ; const userModifiedSrc = onRetry ( newSrc , url , currentCollector ) ; if ( userModifiedSrc === null ) { return callOriginalOnError ( ) ; } if ( typeof userModifiedSrc !== 'string' ) { throw new Error ( 'a string should be returned in `onRetry` function' ) ; } if ( currentCollector [ retryTimesProp ] <= maxRetryCount ) { if ( realElement instanceof ScriptElementCtor ) { loadNextScript ( realElement , userModifiedSrc , noop , true ) ; } else if ( realElement instanceof LinkElementCtor ) { loadNextLink ( realElement , userModifiedSrc ) ; } } else { callOriginalOnError ( ) ; } } ; return ; } if ( key === 'onload' ) { self [ innerOnloadProp ] = newVal ; self [ innerProxyProp ] . onload = function ( event ) { if ( newVal && ! newVal . _called ) { newVal . _called = true ; newVal . call ( self [ innerProxyProp ] , event ) ; } } ; return ; } ; ( realElement as any ) [ key ] = newVal ; } , get ( ) { return ( self [ innerProxyProp ] as any ) [ key ] ; } } ; } return descriptor ; } , { } as PropertyDescriptorMap ) ; } ; const createHookedElement = function ( $element , opts ) : HookedElement { $element . setAttribute ( hookedIdentifier , 'true' ) ; const $hookedElement = { [ innerProxyProp ] : $element , [ innerOnerrorProp ] : noop } ; const descriptors = getHookedElementDescriptors ( $hookedElement , opts ) ; Object . defineProperties ( $hookedElement , descriptors ) ; $hookedElement . onload = noop ; $hookedElement . onerror = noop ; return $hookedElement ; } ; param { InnerAssetsRetryOptions } opts const hookCreateElement = function ( opts ) { const originalCreateElement = doc . createElement ; ( doc as any ) . createElement = function ( name , options ) : any { if ( name === scriptTag || name === linkTag ) { return createHookedElement ( ( originalCreateElement as any ) . call ( doc , name ) , opts ) ; } return originalCreateElement . call ( doc , name , options ) ; } ; } ; param { any } target const hookPrototype = function ( target ) { const functionKeys = Object . keys ( target ) . filter ( key => isFunctionProperty ( target , key ) ) ; functionKeys . forEach ( key => { const originalFunc = target [ key ] ; target [ key ] = function ( ) : any { const args = [ ] . slice . call ( arguments ) . map ( ( item ) => { if ( ! item ) return item ; return hasOwn . call ( item , innerProxyProp ) ? item [ innerProxyProp ] : item ; } ) ; return originalFunc . apply ( this , args ) ; } ; } ) ; } ; param { InnerAssetsRetryOptions } opts returns export default function initAsync ( opts ) { hookCreateElement ( opts ) ; if ( typeof Node !== 'undefined' ) { hookPrototype ( Node . prototype ) ; } if ( typeof Element !== 'undefined' ) { hookPrototype ( Element . prototype ) ; } return retryCollector ; }