import { Logger } from "@adpt/utils" ; import stringify from "json-stable-stringify" ; import { InternalError } from "../error" ; import { AdaptElement , AdaptElementOrNull , AnyProps , isMountedElement , } from "../jsx" ; import { Action , ActionInfo , ChangeType , Plugin , PluginOptions , } from "./deploy_types" ; export interface WidgetPair < E extends AdaptElement , O extends object > { queryDomainKey : QueryDomainKey ; actionInfo : ActionInfo ; element ? : E ; observed ? : O ; } type WidgetActions < E extends AdaptElement , O extends object > = Record < ChangeType , WidgetPair < E , O > [ ] > ; export interface QueryDomain < Id , Secret > { id : Id ; secret : Secret ; } type QueryDomainKey = string ; export type WidgetId = string ; export interface WidgetChange < E extends AdaptElement > { id : WidgetId ; from ? : E ; to ? : E ; } interface Expected < E extends AdaptElement > { [ queryDomainKey ] : WidgetChange < E > [ ] ; } export interface Observed < O extends object > { [ queryDomainKey ] : O [ ] ; } type GetId < T extends object > = ( o : T ) => WidgetId ; type ComputeChanges < E extends AdaptElement , O extends object > = ( e : WidgetChange < E > , o : O | undefined ) => ActionInfo ; type TranslateAction < QD extends QueryDomain < any , any > , E extends AdaptElement , O extends object > = ( d : QD , p : WidgetPair < E , O > ) => void | Promise < void > ; export abstract class WidgetPlugin < WidgetElem extends AdaptElement , WidgetObs extends object , QDomain extends QueryDomain < any , any > > implements Plugin < Observed < WidgetObs > > { deployID_ ? : string ; log_ ? : Logger ; dataDir_ ? : string ; queryDomains = new Map < QueryDomainKey , QDomain > ( ) ; findElems : ( dom : AdaptElementOrNull ) => WidgetElem [ ] ; getElemQueryDomain : ( el : WidgetElem ) => QDomain ; getWidgetTypeFromElem : ( el : WidgetElem ) => string ; getWidgetTypeFromObs : ( obs : WidgetObs ) => string ; getWidgetIdFromElem : ( el : WidgetElem ) => WidgetId ; getWidgetIdFromObs : ( obs : WidgetObs ) => WidgetId ; computeChanges : ComputeChanges < WidgetElem , WidgetObs > ; getObservations : ( domain : QDomain , deployID : string , elemsInQDomain : WidgetChange < WidgetElem > [ ] ) => Promise < WidgetObs [ ] > ; createWidget : ( domain : QDomain , deployID : string , resource : WidgetPair < WidgetElem , WidgetObs > ) => Promise < void > ; destroyWidget : ( domain : QDomain , deployID : string , resource : WidgetPair < WidgetElem , WidgetObs > ) => Promise < void > ; modifyWidget : ( domain : QDomain , deployID : string , resource : WidgetPair < WidgetElem , WidgetObs > ) => Promise < void > ; async start ( options ) { this . deployID_ = options . deployID ; this . log_ = options . log ; this . dataDir_ = options . dataDir ; } createExpected ( oldDom , newDom , createQueryDomains = false ) { const ret = { } ; const changes = new Map < WidgetId , WidgetChange < WidgetElem > > ( ) ; const addElems = ( dom , type ) => { this . findElems ( dom ) . forEach ( ( el ) => { const id = this . getWidgetIdFromElem ( el ) ; let change = changes . get ( id ) ; if ( change ) { change [ type ] = el ; return ; } change = { id , [ type ] : el } ; changes . set ( id , change ) ; const domain = this . getElemQueryDomain ( el ) ; const qdKey = makeQueryDomainKey ( domain ) ; const qdChangeList = ret [ qdKey ] || [ ] ; ret [ qdKey ] = qdChangeList ; qdChangeList . push ( change ) ; if ( createQueryDomains && this . queryDomains . get ( qdKey ) == null ) { this . queryDomains . set ( qdKey , domain ) ; } } ) ; } ; addElems ( oldDom , "from" ) ; addElems ( newDom , "to" ) ; return ret ; } async observe ( oldDom , dom ) { const elemsInQDomain = this . createExpected ( oldDom , dom , true ) ; const obs = { } ; for ( const [ key , domain ] of this . queryDomains . entries ( ) ) { obs [ key ] = await this . getObservations ( domain , this . deployID , elemsInQDomain [ key ] ) ; } return obs ; } analyze ( oldDom , dom , obs ) { const deployID = this . deployID ; const expected = this . createExpected ( oldDom , dom ) ; const actions = diffObservations < WidgetElem , WidgetObs > ( expected , obs , ( o ) => this . getWidgetIdFromObs ( o ) , ( el , o ) => this . computeChanges ( el , o ) ) ; const ret = [ ] ; this . translatePairs ( ret , actions . create , "Creating" , ( d , p ) => this . createWidget ( d , deployID , p ) ) ; this . translatePairs ( ret , actions . modify , "Modifying" , ( d , p ) => this . modifyWidget ( d , deployID , p ) ) ; this . translatePairs ( ret , actions . replace , "Replacing" , async ( d , p ) => { await this . destroyWidget ( d , deployID , p ) ; await this . createWidget ( d , deployID , p ) ; } ) ; this . translatePairs ( ret , actions . delete , "Destroying" , ( d , p ) => this . destroyWidget ( d , deployID , p ) ) ; this . translatePairs ( ret , actions . none , "Not modifying" , ( ) => { } ) ; return ret ; } async finish ( ) { this . log_ = undefined ; } get deployID ( ) : string { if ( this . deployID_ == null ) { throw new InternalError ( `deployID not initialized yet` ) ; } return this . deployID_ ; } get dataDir ( ) : string { if ( this . dataDir_ == null ) { throw new Error ( `Internal error: dataDir not initialized yet` ) ; } return this . dataDir_ ; } log = ( arg , ... args ) : void => { if ( this . log_ ) this . log_ ( arg , ... args ) ; } ; queryDomain ( key ) { return this . queryDomains . get ( key ) ; } widgetInfo ( pair ) { let type ; let id ; let key ; const el = pair . element ; if ( el != null ) { if ( ! isMountedElement ( el ) ) throw new InternalError ( `element not mounted` ) ; type = this . getWidgetTypeFromElem ( el ) ; id = this . getWidgetIdFromElem ( el ) ; key = el . props . key ; } else if ( pair . observed !== undefined ) { type = this . getWidgetTypeFromObs ( pair . observed ) ; id = this . getWidgetIdFromObs ( pair . observed ) ; } else { throw new InternalError ( `WidgetPair with no content` ) ; } return { type , id , key } ; } /**
     * Translate WidgetPairs into plugin Actions
     */ translatePairs ( actions , pairs , actionType , action ) { for ( const p of pairs ) { const { type , id , key } = this . widgetInfo ( p ) ; const k = key ? ` '${ key }'` : "" ; const description = `${ actionType } ${ type }${ k } (id=${ id })` ; const domain = this . queryDomain ( p . queryDomainKey ) ; if ( domain == null ) throw new InternalError ( `domain null` ) ; actions . push ( { ... p . actionInfo , act : async ( ) => { try { await action ( domain , p ) ; } catch ( err ) { const path = p . element ? getPath ( p . element ) : "" ; throw new Error ( `An error occurred while ${ description }` + `${ path }: ${ err . message || err }` ) ; } } } ) ; } } } function getPath ( el ) { if ( isMountedElement ( el ) ) return ` [${ el . path }]` ; return "" ; } function makeQueryDomainKey ( queryDomain ) { return stringify ( queryDomain . id ) ; } function diffArrays < E extends AdaptElement , O extends object > ( queryDomainKey , expected , observed , observedId , computeChanges , actions ) { const obsMap = new Map ( observed . map ( ( o ) => [ observedId ( o ) , o ] as [ string , O ] ) ) ; let actionInfo ; for ( const e of expected ) { const o = obsMap . get ( e . id ) ; if ( o !== undefined ) obsMap . delete ( e . id ) ; actionInfo = computeChanges ( e , o ) ; const pair = { queryDomainKey , actionInfo } ; const actionList = actions [ actionInfo . type ] ; switch ( actionInfo . type ) { case ChangeType . create : actionList . push ( { ... pair , element : e . to } ) ; break ; case ChangeType . none : const element = e . to || e . from ; if ( element == null ) throw new InternalError ( `WidgetChange with no 'from' or 'to' properties` ) ; actionList . push ( { ... pair , element } ) ; break ; case ChangeType . delete : if ( o == null ) { throw new Error ( `Error in deployment plugin. Plugin computeChanges ` + `method returned ChangeType.delete for Element type ` + `${ e . from ! . componentName } but no corresponding ` + `object was observed in the environment.` ) ; } actionList . push ( { ... pair , observed : o } ) ; break ; case ChangeType . modify : case ChangeType . replace : actionList . push ( { ... pair , element : e . to , observed : o } ) ; break ; } } for ( const [ id , o ] of obsMap ) { actionInfo = computeChanges ( { id } , o ) ; actions . delete . push ( { queryDomainKey , actionInfo , observed : o } ) ; } } function diffObservations < E extends AdaptElement , O extends object > ( expected , observed , observedId , computeChanges ) { const actions = { create : [ ] , delete : [ ] , modify : [ ] , none : [ ] , replace : [ ] , } ; observed = { ... observed } ; for ( const key of Object . keys ( expected ) ) { diffArrays ( key , expected [ key ] , observed [ key ] || [ ] , observedId , computeChanges , actions ) ; delete observed [ key ] ; } for ( const key of Object . keys ( observed ) ) { diffArrays ( key , [ ] , observed [ key ] , observedId , computeChanges , actions ) ; } return actions ; }