From aeee1b8446d1b462faea994913df8856a15ca055 Mon Sep 17 00:00:00 2001 From: Jesse Date: Sun, 12 Jan 2025 18:25:41 +0100 Subject: [PATCH] Further async improvements (#1070) - Adds a set of asynchronous cloners able to be used in async methods - Updates setInterval to await the update before processing a new one. - Updates various BotGen methods to remove nested promises and removing a few unnecessary for loops. --- project/src/controllers/BotController.ts | 76 ++++++++++--------- project/src/generators/BotGenerator.ts | 7 +- project/src/services/CreateProfileService.ts | 2 +- project/src/utils/App.ts | 4 +- project/src/utils/cloners/ICloner.ts | 1 + project/src/utils/cloners/JsonCloner.ts | 10 +++ project/src/utils/cloners/RecursiveCloner.ts | 33 ++++++++ project/src/utils/cloners/StructuredCloner.ts | 10 +++ 8 files changed, 103 insertions(+), 40 deletions(-) diff --git a/project/src/controllers/BotController.ts b/project/src/controllers/BotController.ts index 880b8c91..16730c04 100644 --- a/project/src/controllers/BotController.ts +++ b/project/src/controllers/BotController.ts @@ -182,8 +182,8 @@ export class BotController { this.pmcConfig.allPMCsHavePlayerNameWithRandomPrefixChance, ); - const conditionPromises: Promise[] = []; - for (const condition of request.conditions) { + // Map conditions to promises for bot generation + const conditionPromises = request.conditions.map(async (condition) => { const botGenerationDetails = this.getBotGenerationDetailsForWave( condition, pmcProfile, @@ -193,14 +193,11 @@ export class BotController { this.botHelper.isBotPmc(condition.Role), ); - conditionPromises.push(this.generateWithBotDetails(condition, botGenerationDetails, sessionId)); - } + // Generate bots for the current condition + await this.generateWithBotDetails(condition, botGenerationDetails, sessionId); + }); - await Promise.all(conditionPromises) - .then((p) => Promise.all(p)) - .catch((ex) => { - this.logger.error(ex); - }); + await Promise.all(conditionPromises); return []; } @@ -301,26 +298,36 @@ export class BotController { // Get number of bots we have in cache const botCacheCount = this.botGenerationCacheService.getCachedBotCount(cacheKey); - const botPromises: Promise[] = []; - if (botCacheCount > botGenerationDetails.botCountToGenerate) { + + if (botCacheCount >= botGenerationDetails.botCountToGenerate) { + this.logger.debug(`Cache already has sufficient bots: ${botCacheCount}`); return; } // We're below desired count, add bots to cache + const botsToGenerate = botGenerationDetails.botCountToGenerate - botCacheCount; const progressWriter = new ProgressWriter(botGenerationDetails.botCountToGenerate); - for (let i = 0; i < botGenerationDetails.botCountToGenerate; i++) { - const detailsClone = this.cloner.clone(botGenerationDetails); - botPromises.push(this.generateSingleBotAndStoreInCache(detailsClone, sessionId, cacheKey)); - progressWriter.increment(); - } - return await Promise.all(botPromises).then(() => { - this.logger.debug( - `Generated ${botGenerationDetails.botCountToGenerate} ${botGenerationDetails.role} (${ - botGenerationDetails.eventRole ?? botGenerationDetails.role ?? "" - }) ${botGenerationDetails.botDifficulty} bots`, - ); + this.logger.debug(`Generating ${botsToGenerate} bots for cacheKey: ${cacheKey}`); + + const botGenerationPromises = Array.from({ length: botsToGenerate }, async (_, i) => { + try { + const detailsClone = await this.cloner.cloneAsync(botGenerationDetails); + await this.generateSingleBotAndStoreInCache(detailsClone, sessionId, cacheKey); + progressWriter.increment(); + } catch (error) { + this.logger.error(`Failed to generate bot #${i + 1}: ${error.message}`); + } }); + + // Use allSettled here, this allows us to continue even if one of the promises is rejected + await Promise.allSettled(botGenerationPromises); + + this.logger.debug( + `Generated ${botGenerationDetails.botCountToGenerate} ${botGenerationDetails.role} (${ + botGenerationDetails.eventRole ?? botGenerationDetails.role ?? "" + }) ${botGenerationDetails.botDifficulty} bots`, + ); } /** @@ -335,7 +342,7 @@ export class BotController { sessionId: string, cacheKey: string, ): Promise { - const botToCache = this.botGenerator.prepareAndGenerateBot(sessionId, botGenerationDetails); + const botToCache = await this.botGenerator.prepareAndGenerateBot(sessionId, botGenerationDetails); this.botGenerationCacheService.storeBots(cacheKey, [botToCache]); // Store bot details in cache so post-raid PMC messages can use data @@ -422,19 +429,18 @@ export class BotController { // Check cache for bot using above key if (!this.botGenerationCacheService.cacheHasBotWithKey(cacheKey)) { - const botPromises: Promise[] = []; - // No bot in cache, generate new and return one - for (let i = 0; i < botGenerationDetails.botCountToGenerate; i++) { - botPromises.push(this.generateSingleBotAndStoreInCache(botGenerationDetails, sessionId, cacheKey)); - } + // No bot in cache, generate new and store in cache + await Promise.all( + Array.from({ length: botGenerationDetails.botCountToGenerate }).map( + async () => await this.generateSingleBotAndStoreInCache(botGenerationDetails, sessionId, cacheKey), + ), + ); - await Promise.all(botPromises).then(() => { - this.logger.debug( - `Generated ${botGenerationDetails.botCountToGenerate} ${botGenerationDetails.role} (${ - botGenerationDetails.eventRole ?? "" - }) ${botGenerationDetails.botDifficulty} bots`, - ); - }); + this.logger.debug( + `Generated ${botGenerationDetails.botCountToGenerate} ${botGenerationDetails.role} (${ + botGenerationDetails.eventRole ?? "" + }) ${botGenerationDetails.botDifficulty} bots`, + ); } const desiredBot = this.botGenerationCacheService.getBot(cacheKey); diff --git a/project/src/generators/BotGenerator.ts b/project/src/generators/BotGenerator.ts index 6f296643..29ae7624 100644 --- a/project/src/generators/BotGenerator.ts +++ b/project/src/generators/BotGenerator.ts @@ -111,7 +111,10 @@ export class BotGenerator { * @param botGenerationDetails details on how to generate bots * @returns constructed bot */ - public prepareAndGenerateBot(sessionId: string, botGenerationDetails: IBotGenerationDetails): IBotBase { + public async prepareAndGenerateBot( + sessionId: string, + botGenerationDetails: IBotGenerationDetails, + ): Promise { const preparedBotBase = this.getPreparedBotBase( botGenerationDetails.eventRole ?? botGenerationDetails.role, // Use eventRole if provided, botGenerationDetails.side, @@ -122,7 +125,7 @@ export class BotGenerator { const botRole = botGenerationDetails.isPmc ? preparedBotBase.Info.Side // Use side to get usec.json or bear.json when bot will be PMC : botGenerationDetails.role; - const botJsonTemplateClone = this.cloner.clone(this.botHelper.getBotTemplate(botRole)); + const botJsonTemplateClone = await this.cloner.cloneAsync(this.botHelper.getBotTemplate(botRole)); if (!botJsonTemplateClone) { this.logger.error(`Unable to retrieve: ${botRole} bot template, cannot generate bot of this type`); } diff --git a/project/src/services/CreateProfileService.ts b/project/src/services/CreateProfileService.ts index 73d0bf9a..3f958ecd 100644 --- a/project/src/services/CreateProfileService.ts +++ b/project/src/services/CreateProfileService.ts @@ -49,7 +49,7 @@ export class CreateProfileService { public async createProfile(sessionID: string, info: IProfileCreateRequestData): Promise { const account = this.saveServer.getProfile(sessionID).info; - const profileTemplateClone: ITemplateSide = this.cloner.clone( + const profileTemplateClone: ITemplateSide = await this.cloner.cloneAsync( this.databaseService.getProfiles()[account.edition][info.side.toLowerCase()], ); const pmcData = profileTemplateClone.character; diff --git a/project/src/utils/App.ts b/project/src/utils/App.ts index 492095c4..b5ee47d0 100644 --- a/project/src/utils/App.ts +++ b/project/src/utils/App.ts @@ -64,8 +64,8 @@ export class App { await onLoad.onLoad(); } - setInterval(() => { - this.update(this.onUpdateComponents); + setInterval(async () => { + await this.update(this.onUpdateComponents); }, 5000); } diff --git a/project/src/utils/cloners/ICloner.ts b/project/src/utils/cloners/ICloner.ts index 77c05418..c8128def 100644 --- a/project/src/utils/cloners/ICloner.ts +++ b/project/src/utils/cloners/ICloner.ts @@ -1,3 +1,4 @@ export interface ICloner { clone(obj: T): T; + cloneAsync(obj: T): Promise; } diff --git a/project/src/utils/cloners/JsonCloner.ts b/project/src/utils/cloners/JsonCloner.ts index 7b37dca5..ddd8a2d2 100644 --- a/project/src/utils/cloners/JsonCloner.ts +++ b/project/src/utils/cloners/JsonCloner.ts @@ -6,4 +6,14 @@ export class JsonCloner implements ICloner { public clone(obj: T): T { return JSON.parse(JSON.stringify(obj)); } + + public async cloneAsync(obj: T): Promise { + return new Promise((resolve, reject) => { + try { + resolve(JSON.parse(JSON.stringify(obj))); + } catch (error) { + reject(error); + } + }); + } } diff --git a/project/src/utils/cloners/RecursiveCloner.ts b/project/src/utils/cloners/RecursiveCloner.ts index 6fd8c0fa..839a35ae 100644 --- a/project/src/utils/cloners/RecursiveCloner.ts +++ b/project/src/utils/cloners/RecursiveCloner.ts @@ -44,4 +44,37 @@ export class RecursiveCloner implements ICloner { throw new Error(`Cant clone ${JSON.stringify(obj)}`); } + + public async cloneAsync(obj: T): Promise { + // if null or undefined return it as is + if (obj === null || obj === undefined) return obj; + + const typeOfObj = typeof obj; + + // no need to clone these types, they are primitives + if (RecursiveCloner.primitives.has(typeOfObj)) { + return obj; + } + + // clone the object types + if (typeOfObj === "object") { + if (Array.isArray(obj)) { + const objArr = obj as Array; + const clonedArray = await Promise.all(objArr.map(async (v) => await this.cloneAsync(v))); + return clonedArray as T; + } + + const newObj: Record = {}; + const clonePromises = Object.keys(obj).map(async (key) => { + const value = (obj as Record)[key]; + newObj[key] = await this.cloneAsync(value); + }); + + await Promise.all(clonePromises); + return newObj as T; + } + + // Handle unsupported types + throw new Error(`Cannot clone ${JSON.stringify(obj)}`); + } } diff --git a/project/src/utils/cloners/StructuredCloner.ts b/project/src/utils/cloners/StructuredCloner.ts index 267a7d63..e02d8e5f 100644 --- a/project/src/utils/cloners/StructuredCloner.ts +++ b/project/src/utils/cloners/StructuredCloner.ts @@ -6,4 +6,14 @@ export class StructuredCloner implements ICloner { public clone(obj: T): T { return structuredClone(obj); } + + public async cloneAsync(obj: T): Promise { + return new Promise((resolve, reject) => { + try { + resolve(structuredClone(obj)); + } catch (error) { + reject(error); + } + }); + } }