import { DeepPartial } from '../utils/DeepPartial' ; import { deepMerge } from '../utils/DeepMerge' ; import { genID } from '../utils' ; import { deepCopy } from '../utils/DeepCopy' ; import { Logger } from '../Logger' ; import { Match } from '../Match' ; import { Station , BOT_DIR } from '../Station' ; import { MissingFilesError } from '../DimensionError' ; import { Design } from '../Design' ; import { Tournament } from '../Tournament' ; import { Plugin } from '../Plugin' ; import { Database } from '../Plugin/Database' ; import { Storage } from '../Plugin/Storage' ; import { existsSync , mkdirSync } from 'fs' ; import { Agent } from '../Agent' ; import { AgentClassTypeGuards } from '../utils/TypeGuards' ; /**
 * Some standard database type strings
 */ export enum DatabaseType { /**
     * Represents no database used
     */ NONE = 'none' , /**
     * Represents mongodb database used
     */ MONGO = 'mongo' , /**
     * Firestore DB is used
     */ FIRESTORE = 'firestore' } export enum StorageType { /**
     * Represents no storage used, all files stored locally on devide
     */ NONE = 'none' , /**
     * Represents gcloud storage used
     */ GCLOUD = 'gcloud' , /** Using local file system for storage*/ FS = 'fs-storage' } /**
 * An id generated using nanoid
 */ export type NanoID = string ; /**
 * Dimension configurations
 */ export interface DimensionConfigs { /** Name of the dimension */ name : string ; default activateStation : boolean ; default observe : boolean ; /** The logging level for this Dimension */ loggingLevel : Logger . LEVEL ; /** The default match configurations to use when creating matches using this Dimension */ defaultMatchConfigs : DeepPartial < Match . Configs > ; /** An overriding ID to use for the dimension instead of generating a new one */ id : NanoID ; default secureMode : boolean ; default DatabaseType . NONE backingDatabase : string | DatabaseType ; default DatabaseType . NONE backingStorage : string | StorageType ; /**
     * Station configs to use
     */ stationConfigs : DeepPartial < Station . Configs > ; default createBotDirectories : boolean ; } /**
 * The Dimension framework for intiating a  Design to then run instances of a  Match or
 *  Tournament on.
  export class Dimension { /**
     * A map of the matches running in this Dimension
     */ public matches : Map < NanoID , Match > = new Map ( ) ; /**
     * A map of the tournaments in this Dimension.
     */ public tournaments : Map < NanoID , Tournament > = new Map ( ) ; /**
     * This Dimension's name
     */ public name : string ; /**
     * This dimension's ID. It is always a 6 character NanoID unless overrided through the  DimensionConfigs public id : NanoID ; /**
     * Logger
     */ public log = new Logger ( ) ; /**
     * The database plugin being used. Allows Dimensions to interact with a database and store  Match ,
     *  Tournament , and user data, allowing for data persistance across instances.
      public databasePlugin : Database ; /**
     * The storage plugin being used. Allows Dimensions to interact with a storage service and store user object data,
     * particuarly bot file uploads
     */ public storagePlugin : Storage ; /**
     * The Station associated with this Dimension and current node instance
     */ public static Station : Station = null ; /**
     * Stats
     */ public statistics = { tournamentsCreated : 0 , matchesCreated : 0 , } ; /**
     * Dimension configs. Set to defaults
     */ public configs : DimensionConfigs = { name : '' , activateStation : true , observe : true , loggingLevel : Logger . LEVEL . INFO , defaultMatchConfigs : { secureMode : false , } , secureMode : false , backingDatabase : DatabaseType . NONE , backingStorage : StorageType . NONE , id : 'oLBptg' , stationConfigs : { } , createBotDirectories : true , } ; /**
     * Indicator of whether cleanup was called already or not
     */ private cleaningUp : Promise < any > = null ; constructor ( public design , configs = { } ) { this . configs = deepMerge ( this . configs , configs ) ; if ( ! configs . id ) { this . id = Dimension . genDimensionID ( ) ; } else { this . id = configs . id ; } this . log . level = this . configs . loggingLevel ; if ( this . configs . stationConfigs . loggingLevel === undefined ) { this . configs . stationConfigs . loggingLevel = this . configs . loggingLevel ; } if ( this . configs . activateStation === true && Dimension . Station == null ) { Dimension . Station = new Station ( 'Station' , [ ] , this . configs . stationConfigs ) ; } this . configs . defaultMatchConfigs . loggingLevel = this . configs . loggingLevel ; this . design . setLogLevel ( this . configs . loggingLevel ) ; if ( this . configs . name ) { this . name = this . configs . name ; } else { this . name = `dimension_${ this . id }` ; } this . log . identifier = `${ this . name } Log` ; if ( this . configs . secureMode ) { this . setupSecurity ( ) ; } else { this . log . warn ( `WARNING: Running in non-secure mode. You will not be protected against malicious bots` ) ; } this . configs . defaultMatchConfigs . secureMode = this . configs . secureMode ; process . on ( 'exit' , async ( ) => { try { await this . cleanup ( ) ; } catch ( err ) { console . error ( err ) ; } process . exit ( ) ; } ) ; process . on ( 'SIGINT' , async ( ) => { try { await this . cleanup ( ) ; } catch ( err ) { console . error ( err ) ; } process . exit ( ) ; } ) ; if ( this . configs . observe === true && Dimension . Station != null ) Dimension . Station . observe ( this ) ; this . log . info ( `Created Dimension - ID: ${ this . id }, Name: ${ this . name }` ) ; this . log . detail ( 'Dimension Configs' , this . configs ) ; if ( ! existsSync ( BOT_DIR ) && this . configs . createBotDirectories ) { mkdirSync ( BOT_DIR , { recursive : true } ) ; } } /**
     * Create a match with the given files and any optional  Match . Configs . Resolves with the initialized
     *  Match object as specified by the  Design of this  Dimension *
     * Rejects if an error occurs.
     *
     *  param files param matchOptions param configs - Configurations that are  Design dependent
      public async createMatch ( files , configs ? ) { if ( ! files . length ) { throw new MissingFilesError ( 'No files provided for match' ) ; } let matchConfigs = deepCopy ( this . configs . defaultMatchConfigs ) ; matchConfigs = deepMerge ( matchConfigs , configs ) ; let match ; if ( AgentClassTypeGuards . isGenerationMetaData_FilesOnly ( files ) ) { match = new Match ( this . design , files , matchConfigs , this ) ; } else { match = new Match ( this . design , files , matchConfigs , this ) ; } this . statistics . matchesCreated ++ ; this . matches . set ( match . id , match ) ; await match . initialize ( ) ; return match ; } /**
     * Runs a match with the given files and any optional  Match . Configs . It rejects if an error occurs. Some
     * errors include  MatchDestroyedError which happens when  Match . destroy is called.
     *
     * This also automatically stores matches into the  Database if database is active and configured to save
     *
     * Resolves with the results of the match as specified by the  Design of this  Dimension param files param matchOptions param configs public async runMatch ( files , configs ? ) { const match = await this . createMatch ( files , configs ) ; const results = await match . run ( ) ; if ( this . hasDatabase ( ) ) { if ( this . databasePlugin . configs . saveMatches ) { this . databasePlugin . storeMatch ( match , this . id ) ; } } return results ; } param files param configs see Tournament for the different tournament types
     *  returns public createTournament ( files , configs ) { const id = Tournament . genTournamentClassID ( ) ; let newTourney ; if ( configs . loggingLevel === undefined ) { configs . loggingLevel = this . log . level ; } const dimensionDefaultMatchConfigs = deepCopy ( this . configs . defaultMatchConfigs ) ; configs = deepMerge ( { defaultMatchConfigs : dimensionDefaultMatchConfigs } , configs ) ; switch ( configs . type ) { case Tournament . Type . LADDER : newTourney = new Tournament . Ladder ( this . design , files , configs , id , this ) ; break ; case Tournament . Type . ELIMINATION : newTourney = new Tournament . Elimination ( this . design , files , configs , id , this ) ; break ; } this . statistics . tournamentsCreated ++ ; this . tournaments . set ( newTourney . id , newTourney ) ; return newTourney ; } /**
     * Get the station
     */ getStation ( ) { return Dimension . Station ; } /**
     * Removes a match by id. Returns true if removed, false if nothing was removed
     */ public async removeMatch ( matchID ) { if ( this . matches . has ( matchID ) ) { const match = this . matches . get ( matchID ) ; await match . destroy ( ) ; return this . matches . delete ( matchID ) ; } return false ; } /**
     * Sets up necessary security and checks if everything is in place
     */ private setupSecurity ( ) { } /**
     * Generates a 6 character nanoID string for identifying dimensions
     */ public static genDimensionID ( ) { return genID ( 6 ) ; } param plugin public async use ( plugin ) { switch ( plugin . type ) { case Plugin . Type . DATABASE : this . log . info ( 'Attaching Database Plugin ' + plugin . name ) ; this . configs . backingDatabase = 'unknown' ; this . databasePlugin = < Database > plugin ; await this . databasePlugin . initialize ( this ) ; break ; case Plugin . Type . STORAGE : this . log . info ( 'Attaching Storage Plugin ' + plugin . name ) ; this . configs . backingStorage = 'unknown;' ; this . storagePlugin = < Storage > plugin ; await this . storagePlugin . initialize ( this ) ; break ; default : break ; } await plugin . manipulate ( this ) ; } /**
     * Returns true if dimension has a database backing it
     */ public hasDatabase ( ) { return ( this . databasePlugin !== undefined && this . configs . backingDatabase !== DatabaseType . NONE ) ; } /**
     * Returns true if dimension has a storage plugin backing it
     */ public hasStorage ( ) { return ( this . storagePlugin && this . configs . backingStorage !== StorageType . NONE ) ; } /**
     * Cleanup function that cleans up any resources used and related to this dimension. For use right before
     * process exits and during testing.
     */ async cleanup ( ) { if ( this . cleaningUp ) { return this . cleaningUp ; } this . log . info ( 'Cleaning up' ) ; const cleanUpPromises = [ ] ; cleanUpPromises . push ( this . cleanupMatches ( ) ) ; cleanUpPromises . push ( this . cleanupTournaments ( ) ) ; if ( this . getStation ( ) ) { cleanUpPromises . push ( this . getStation ( ) . stop ( ) ) ; } this . cleaningUp = Promise . all ( cleanUpPromises ) ; await this . cleaningUp ; } async cleanupMatches ( ) { const cleanUpPromises = [ ] ; this . matches . forEach ( ( match ) => { cleanUpPromises . push ( match . destroy ( ) ) ; } ) ; return Promise . all ( cleanUpPromises ) ; } async cleanupTournaments ( ) { const cleanUpPromises = [ ] ; this . tournaments . forEach ( ( tournament ) => { cleanUpPromises . push ( tournament . destroy ( ) ) ; } ) ; return Promise . all ( cleanUpPromises ) ; } } param design param configs export function create ( design , configs ? ) { return new Dimension ( design , configs ) ; }