import { NodeCG } from "nodecg-types/types/server" ; import { ObjectMap , Service , ServiceInstance } from "./service" ; import { emptySuccess , error , Result } from "./utils/result" ; import { ServiceManager } from "./serviceManager" ; import { BundleManager } from "./bundleManager" ; import Ajv from "ajv" ; import { EventEmitter } from "events" ; import { Logger } from "./utils/logger" ; /**
 * Manages instances of services and their configs/clients.
 */ export class InstanceManager extends EventEmitter { private serviceInstances : ObjectMap < ServiceInstance < unknown , unknown > > = { } ; private ajv = new Ajv ( ) ; constructor ( private readonly nodecg , private readonly services , private readonly bundles ) { super ( ) ; bundles . on ( "reregisterInstance" , ( serviceInstance ? ) => this . reregisterHandlersOfInstance ( serviceInstance ) ) ; } param instanceName return getServiceInstance ( instanceName ) { return this . serviceInstances [ instanceName ] ; } return { ObjectMap < ServiceInstance < unknown , unknown > > } getServiceInstances ( ) { return this . serviceInstances ; } param serviceType param instanceName return createServiceInstance ( serviceType , instanceName ) { if ( ! instanceName ) { return error ( "Instance name must not be empty." ) ; } if ( this . serviceInstances [ instanceName ] !== undefined ) { return error ( "A service instance with the same name already exists." ) ; } const svcResult = this . services . getService ( serviceType ) ; if ( svcResult . failed ) { return error ( "A service of this service type hasn't been registered." ) ; } const service = svcResult . result ; const inst = { serviceType : service . serviceType , config : service . defaultConfig , client : undefined } ; this . serviceInstances [ instanceName ] = inst ; this . emit ( "change" ) ; this . nodecg . log . info ( `Service instance "${ instanceName }" of service "${ service . serviceType }" has been successfully created.` ) ; if ( service . requiresNoConfig ) { this . updateInstanceClient ( inst , instanceName , service ) ; } return emptySuccess ( ) ; } param instanceName return deleteServiceInstance ( instanceName ) { const instance = this . serviceInstances [ instanceName ] ; if ( ! instance ) return false ; this . nodecg . log . info ( `Deleting service instance "${ instanceName }"` ) ; if ( instance . client ) { this . nodecg . log . info ( `Stopping client of service instance "${ instanceName }"...` ) ; const svc = this . services . getService ( instance . serviceType ) ; if ( svc . failed ) { this . nodecg . log . error ( `Failed to stop client: ${ svc . errorMessage }` ) ; } else { this . nodecg . log . info ( `Successfully stopped client of service instance "${ instanceName }".` ) ; try { svc . result . stopClient ( instance . client , new Logger ( instanceName , this . nodecg ) ) ; } catch ( e ) { this . nodecg . log . error ( `Couldn't stop service instance: ${ e }` ) ; } } } const deps = this . bundles . getBundleDependencies ( ) ; Object . entries ( deps ) . forEach ( ( [ bundleName , deps ] ) => deps . filter ( ( d ) => d . serviceInstance === instanceName ) . forEach ( ( d ) => this . bundles . unsetServiceDependency ( bundleName , d . serviceType ) ) ) ; delete this . serviceInstances [ instanceName ] ; this . emit ( "change" ) ; return true ; } param instanceName param config param validation return updateInstanceConfig ( instanceName , config , validation = true ) { const inst = this . serviceInstances [ instanceName ] ; if ( inst === undefined ) { return Promise . resolve ( error ( "Service instance doesn't exist." ) ) ; } const service = this . services . getService ( inst . serviceType ) ; if ( service . failed ) { return Promise . resolve ( error ( "The service of this instance couldn't be found." ) ) ; } if ( ! validation || service . result . requiresNoConfig ) { inst . config = config ; this . emit ( "change" ) ; return this . updateInstanceClient ( inst , instanceName , service . result ) ; } return ( async ( ) => { if ( service . result . schema ) { const schemaValid = this . ajv . validate ( service . result . schema , config ) ; if ( ! schemaValid ) { return error ( "Config invalid: " + this . ajv . errorsText ( ) ) ; } } try { const validationRes = await service . result . validateConfig ( config , new Logger ( instanceName , this . nodecg ) ) ; if ( validationRes . failed ) { throw validationRes . errorMessage ; } } catch ( err ) { this . nodecg . log . warn ( `Couldn't validate config of ${ service . result . serviceType } instance "${ instanceName }": ${ err }` ) ; return error ( "Config invalid: " + err ) ; } inst . config = config ; this . emit ( "change" ) ; const updateResult = await this . updateInstanceClient ( inst , instanceName , service . result ) ; return updateResult ; } ) ( ) ; } param inst param instanceName param service async updateInstanceClient < R , C > ( inst , instanceName , service ) { const oldClient = inst . client ; if ( inst . config === undefined && ! service . requiresNoConfig ) { inst . client = undefined ; } else { try { const client = await service . createClient ( inst . config ! , new Logger ( instanceName , this . nodecg ) ) ; if ( client . failed ) { throw client . errorMessage ; } else { inst . client = client . result ; } } catch ( err ) { const msg = `The "${ inst . serviceType }" service with the name "${ instanceName }" produced an error while creating a client: ${ err }` ; this . nodecg . log . error ( msg ) ; inst . client = undefined ; return error ( msg ) ; } } this . bundles . handleInstanceUpdate ( inst , instanceName ) ; if ( oldClient !== undefined ) { this . nodecg . log . info ( `Stopping old unused ${ inst . serviceType } client...` ) ; try { service . stopClient ( oldClient , new Logger ( instanceName , this . nodecg ) ) ; } catch ( e ) { this . nodecg . log . error ( `Couldn't stop service instance: ${ e }` ) ; } } return emptySuccess ( ) ; } param instanceName private reregisterHandlersOfInstance ( instanceName ? ) { if ( ! instanceName ) return ; const inst = this . getServiceInstance ( instanceName ) ; if ( ! inst ) { this . nodecg . log . error ( `Can't re-register handlers of instance "${ instanceName }": instance not found` ) ; return ; } const svc = this . services . getService ( inst . serviceType ) ; if ( svc . failed ) { this . nodecg . log . error ( `Can't re-register handlers of instance "${ instanceName }": can't get service: ${ svc . errorMessage }` ) ; return ; } if ( svc . result . reCreateClientToRemoveHandlers ) { this . updateInstanceClient ( inst , instanceName , svc . result ) ; return ; } if ( ! svc . result . removeHandlers ) return ; try { svc . result . removeHandlers ( inst . client ) ; } catch ( err ) { this . nodecg . log . error ( `Can't re-register handlers of instance "${ instanceName }": error while removing handlers: ${ String ( err ) }` ) ; } this . bundles . handleInstanceUpdate ( inst , instanceName ) ; } }