import { HttpClient , HttpHeaders } from '@angular/common/http' ; import { ElementRef , Inject , Injectable , InjectionToken , Renderer2 , RendererFactory2 } from '@angular/core' ; import 'rxjs/add/observable/of' ; import 'rxjs/add/operator/do' ; import { Observable } from 'rxjs/Observable' ; import { Subject } from 'rxjs/Subject' ; import * as _ from 'lodash' ; import { NodeUtils } from './node.utils' ; import { RunService } from './run.service' ; import { SubscriptionService } from './subscription.service' ; export interface Dict { [ field ] : any ; } export interface RequestOptions { params ? : Dict ; headers ? : Dict ; } export const GATEWAY_URL = new InjectionToken < string > ( 'gateway.url' ) ; const SUCCESS = 200 ; export enum Method { GET = 'GET' , POST = 'POST' } export interface BaseParams { from : string ; fullComponentName : string ; path ? : string ; options ? : string ; } export interface RunParams extends BaseParams { runId : string ; [ s ] : string ; } export interface ChildRequest { method : Method ; body : string | Object ; query : RunParams ; } interface ChildResponse { status : number ; body : Object ; } const headers = new HttpHeaders ( { 'Content-type' : 'application/json' } ) ; /**
 * Class for batching transaction requests.
 */ export class TxRequest { private requests : ChildRequest [ ] = [ ] ; private subjects : Subject < any > [ ] = [ ] ; private numComponentsDone = 0 ; private numComponentsTotal : number ; constructor ( private gatewayUrl , private http , private fromNode , private renderer , private rs ) { } /**
     * Set the number of components that are part of the tx
     * The tx request will never be sent until this is done
     */ setNumComponents ( numComponents ) { this . numComponentsTotal = numComponents ; if ( this . isReady ( ) ) { this . send ( ) ; } } /**
     * Add a request to the batch.
     * Returned observable resolves with response (after send is called)
     */ postRequest < T > ( chReq ? ) { const subject = new Subject < T > ( ) ; this . requests . push ( chReq ) ; this . subjects . push ( subject ) ; this . incDoneCountAndMaybeSend ( ) ; return subject . asObservable ( ) ; } /**
     * Report a component in the tx group as not sending a request
     */ postNoRequest ( ) { this . incDoneCountAndMaybeSend ( ) ; } private incDoneCountAndMaybeSend ( ) { this . numComponentsDone += 1 ; if ( this . isReady ( ) ) { this . send ( ) ; } } /**
     * Whether or not the request is ready to send
     */ private isReady ( ) { console . log ( `Checking if req is ready (run id ${ NodeUtils . GetRunId ( this . fromNode ) })` + `: got ${ this . numComponentsDone }/${ this . numComponentsTotal }` ) ; return ( this . numComponentsTotal !== undefined && this . numComponentsDone === this . numComponentsTotal ) ; } /**
     * Gets the context for this tx request.
     * The context is a map `field -> value` for all the fields of the app
     * component containing the tx. Only fields that can be referenced in template
     * exprs are included (e.g., `ngOnInit` is not part of the context)
     */ private getContext ( ) { const appComponentNode = NodeUtils . GetAppComponentNodeContainingNode ( this . fromNode , this . renderer ) ; if ( appComponentNode === null ) { throw new Error ( `App node for ${ NodeUtils . GetFqTagOfNode ( this . fromNode ) } not found` ) ; } const componentId = NodeUtils . GetComponentId ( appComponentNode ) ; const componentInstance = this . rs . getComponentInstance ( componentId ) ; if ( componentInstance === null ) { throw new Error ( `Component instance for id ${ componentId } not found` ) ; } const contextKeys = _ . without ( _ . keys ( componentInstance ) , 'rs' , 'elem' ) ; const context = _ . pickBy ( _ . mapKeys ( _ . pick ( componentInstance , contextKeys ) , ( _value , key ) => key . startsWith ( '___' ) ? key . slice ( 1 ) : key ) , ( value ) => _ . isArray ( value ) || _ . isPlainObject ( value ) || _ . isBoolean ( value ) || _ . isNumber ( value ) || _ . isString ( value ) || value === undefined || value === null ) ; return context ; } /**
     * Send all of the requests.
     */ private send ( ) { if ( this . requests . length === 1 ) { const { method , body , query : params } = this . requests [ 0 ] ; let obs = null ; switch ( method ) { case Method . GET : obs = this . http . get ( this . gatewayUrl , { params } ) ; break ; case Method . POST : obs = this . http . post ( this . gatewayUrl , typeof body === 'object' ? JSON . stringify ( body ) : body , { params , headers } ) ; break ; } const subject = this . subjects [ 0 ] ; console . log ( 'Sending non-tx request' ) ; obs . subscribe ( ( resBody ) => { subject . next ( resBody ) ; subject . complete ( ) ; } , ( error ) => { subject . error ( new Error ( JSON . stringify ( error ) ) ) ; subject . complete ( ) ; } ) ; } else { console . log ( 'Sending TX request' ) ; this . http . post < ChildResponse [ ] > ( this . gatewayUrl , { context : this . getContext ( ) , requests : this . requests } , { headers , params : { isTx : '1' } } ) . subscribe ( ( responses ) => { responses . forEach ( ( { status , body } , i ) => { if ( status === SUCCESS ) { this . subjects [ i ] . next ( body ) ; } else { this . subjects [ i ] . error ( new Error ( JSON . stringify ( body ) ) ) ; } this . subjects [ i ] . complete ( ) ; } ) ; } , ( errors ) => { errors . error . forEach ( ( { unused_status , body } , i ) => { this . subjects [ i ] . error ( new Error ( JSON . stringify ( body ) ) ) ; this . subjects [ i ] . complete ( ) ; } ) ; } ) ; } delete GatewayService . txBatches [ NodeUtils . GetRunId ( this . fromNode ) ] ; } } export class GatewayService { static txBatches : { [ txId ] : TxRequest ; } = { } ; static txPendingNumComponents : { [ txId ] : number ; } = { } ; fromStr : string ; private readonly gatewayHttpUrl : string ; constructor ( gatewayUrl , private http , private renderer , private rs , private ss , private from ) { this . fromStr = this . generateFromStr ( ) ; this . gatewayHttpUrl = `http://${ gatewayUrl }` ; } /**
     * A component must call get, post, or noRequest exactly once
     */ get < T > ( path ? , options ? ) { return this . request < T > ( Method . GET , path , undefined , options ) ; } post < T > ( path ? , body ? , options ? ) { return this . request < T > ( Method . POST , path , body , options ) ; } noRequest ( ) { const runId = NodeUtils . GetRunId ( this . from . nativeElement ) ; const txRequest = this . getTxReq ( runId ) ; txRequest . postNoRequest ( ) ; } subscribe < T > ( path ? , request = { } ) { return this . ss . subscribe < T > ( request , this . buildBaseParams ( path ) ) ; } /**
     * Component path.
     */ protected generateFromStr ( ) { const node = this . from . nativeElement ; const seenComponentNodes = [ ] ; NodeUtils . WalkUpFromNode ( node , this . renderer , undefined , ( vNode ) => { if ( NodeUtils . IsComponent ( vNode ) ) { seenComponentNodes . push ( NodeUtils . GetFqTagOfNode ( vNode ) ) ; } } ) ; return JSON . stringify ( _ . reverse ( seenComponentNodes ) ) ; } /**
     * Gets a TxRequest for runId or makes a new one
     */ public getTxReq ( runId ? ) { let txRequest = GatewayService . txBatches [ runId ] ; if ( txRequest === undefined ) { txRequest = new TxRequest ( this . gatewayHttpUrl , this . http , this . from . nativeElement , this . renderer , this . rs ) ; const maybeNumComponents = GatewayService . txPendingNumComponents [ runId ] ; if ( maybeNumComponents ) { console . log ( `Pulled up pending num components for ${ runId }` ) ; txRequest . setNumComponents ( maybeNumComponents ) ; delete GatewayService . txPendingNumComponents [ runId ] ; } if ( runId ) { GatewayService . txBatches [ runId ] = txRequest ; } } return txRequest ; } private request < T > ( method , path ? , body ? , options ? ) { console . log ( `Sending ${ method } from ${ this . getComponentName ( ) }` ) ; const params = this . buildRunParams ( path , options ) ; const txRequest = this . getTxReq ( params . runId ) ; const individualResponseObservable = txRequest . postRequest < T > ( { method , body , query : params } ) ; if ( ! params . runId ) { txRequest . setNumComponents ( 1 ) ; } return individualResponseObservable ; } private buildBaseParams ( path ? , options ? ) { const params = { from : this . fromStr , fullComponentName : this . getComponentName ( ) } ; if ( path ) { params . path = path ; } if ( options ) { params . options = JSON . stringify ( options ) ; } return params ; } private buildRunParams ( path ? , options ? ) { return { ... this . buildBaseParams ( path , options ) , runId : NodeUtils . GetRunId ( this . from . nativeElement ) } ; } private getComponentName ( ) { return this . from . nativeElement . nodeName . toLowerCase ( ) ; } } export class DesignerGatewayService extends GatewayService { constructor ( gatewayUrl , http , renderer , rs , ss , from ) { super ( gatewayUrl , http , renderer , rs , ss , from ) ; } get fromStr ( ) { return this . generateFromStr ( ) ; } set fromStr ( _s ) { } } @ Injectable ( ) export class GatewayServiceFactory { private readonly renderer : Renderer2 ; constructor ( @ Inject ( GATEWAY_URL ) private gatewayUrl , private http , rendererFactory , private rs , private ss ) { this . renderer = rendererFactory . createRenderer ( null , null ) ; } for ( from ) { const cls = window [ 'dv-designer' ] ? DesignerGatewayService : GatewayService ; return new cls ( this . gatewayUrl , this . http , this . renderer , this . rs , this . ss , from ) ; } }