From b00e7aac95ab08e1141f4a2a9499a81eebeac1d5 Mon Sep 17 00:00:00 2001 From: barsa Date: Tue, 24 Feb 2026 11:57:59 +0900 Subject: [PATCH] refactor: extract Salesforce throttle base guard and rename RealtimeModule - Create SalesforceThrottleBaseGuard abstract base class (read/write guards are thin subclasses) - Rename infra RealtimeModule to RealtimePubSubModule to distinguish from RealtimeApiModule --- apps/bff/src/app.module.ts | 4 +-- .../bff/src/infra/realtime/realtime.module.ts | 2 +- .../guards/salesforce-read-throttle.guard.ts | 36 ++++--------------- .../guards/salesforce-throttle-base.guard.ts | 32 +++++++++++++++++ .../guards/salesforce-write-throttle.guard.ts | 36 ++++--------------- 5 files changed, 47 insertions(+), 63 deletions(-) create mode 100644 apps/bff/src/integrations/salesforce/guards/salesforce-throttle-base.guard.ts diff --git a/apps/bff/src/app.module.ts b/apps/bff/src/app.module.ts index afadbf69..c902118c 100644 --- a/apps/bff/src/app.module.ts +++ b/apps/bff/src/app.module.ts @@ -15,7 +15,7 @@ import { RateLimitModule } from "@bff/core/rate-limiting/index.js"; import { PrismaModule } from "@bff/infra/database/prisma.module.js"; import { RedisModule } from "@bff/infra/redis/redis.module.js"; import { CacheModule } from "@bff/infra/cache/cache.module.js"; -import { RealtimeModule } from "@bff/infra/realtime/realtime.module.js"; +import { RealtimePubSubModule } from "@bff/infra/realtime/realtime.module.js"; import { QueueModule } from "@bff/infra/queue/queue.module.js"; import { AuditModule } from "@bff/infra/audit/audit.module.js"; import { EmailModule } from "@bff/infra/email/email.module.js"; @@ -73,7 +73,7 @@ import { HealthModule } from "@bff/modules/health/health.module.js"; PrismaModule, RedisModule, CacheModule, - RealtimeModule, + RealtimePubSubModule, QueueModule, AuditModule, EmailModule, diff --git a/apps/bff/src/infra/realtime/realtime.module.ts b/apps/bff/src/infra/realtime/realtime.module.ts index b779d83d..cbc6b49c 100644 --- a/apps/bff/src/infra/realtime/realtime.module.ts +++ b/apps/bff/src/infra/realtime/realtime.module.ts @@ -7,4 +7,4 @@ import { RealtimeService } from "./realtime.service.js"; providers: [RealtimePubSubService, RealtimeService], exports: [RealtimeService], }) -export class RealtimeModule {} +export class RealtimePubSubModule {} diff --git a/apps/bff/src/integrations/salesforce/guards/salesforce-read-throttle.guard.ts b/apps/bff/src/integrations/salesforce/guards/salesforce-read-throttle.guard.ts index b35c4a1d..d7b8efcc 100644 --- a/apps/bff/src/integrations/salesforce/guards/salesforce-read-throttle.guard.ts +++ b/apps/bff/src/integrations/salesforce/guards/salesforce-read-throttle.guard.ts @@ -1,33 +1,9 @@ -import { Inject, Injectable, HttpException, HttpStatus } from "@nestjs/common"; -import type { CanActivate, ExecutionContext } from "@nestjs/common"; -import type { Request } from "express"; -import { Logger } from "nestjs-pino"; -import { SalesforceRequestQueueService } from "@bff/infra/queue/services/salesforce-request-queue.service.js"; +import { Injectable } from "@nestjs/common"; +import { SalesforceThrottleBaseGuard } from "./salesforce-throttle-base.guard.js"; @Injectable() -export class SalesforceReadThrottleGuard implements CanActivate { - constructor( - private readonly queue: SalesforceRequestQueueService, - @Inject(Logger) private readonly logger: Logger - ) {} - - canActivate(context: ExecutionContext): boolean { - const state = this.queue.getDegradationState(); - if (!state.degraded) { - return true; - } - - const request = context.switchToHttp().getRequest(); - this.logger.warn("Throttling Salesforce-backed read due to degraded state", { - path: request?.originalUrl ?? request?.url, - reason: state.reason, - cooldownExpiresAt: state.cooldownExpiresAt, - usagePercent: state.usagePercent, - }); - - throw new HttpException( - "We're experiencing high load right now. Please try again in a moment.", - HttpStatus.TOO_MANY_REQUESTS - ); - } +export class SalesforceReadThrottleGuard extends SalesforceThrottleBaseGuard { + protected readonly operationType = "read"; + protected readonly userMessage = + "We're experiencing high load right now. Please try again in a moment."; } diff --git a/apps/bff/src/integrations/salesforce/guards/salesforce-throttle-base.guard.ts b/apps/bff/src/integrations/salesforce/guards/salesforce-throttle-base.guard.ts new file mode 100644 index 00000000..aafd9780 --- /dev/null +++ b/apps/bff/src/integrations/salesforce/guards/salesforce-throttle-base.guard.ts @@ -0,0 +1,32 @@ +import { Inject, HttpException, HttpStatus } from "@nestjs/common"; +import type { CanActivate, ExecutionContext } from "@nestjs/common"; +import type { Request } from "express"; +import { Logger } from "nestjs-pino"; +import { SalesforceRequestQueueService } from "@bff/infra/queue/services/salesforce-request-queue.service.js"; + +export abstract class SalesforceThrottleBaseGuard implements CanActivate { + protected abstract readonly operationType: string; + protected abstract readonly userMessage: string; + + constructor( + protected readonly queue: SalesforceRequestQueueService, + @Inject(Logger) protected readonly logger: Logger + ) {} + + canActivate(context: ExecutionContext): boolean { + const state = this.queue.getDegradationState(); + if (!state.degraded) { + return true; + } + + const request = context.switchToHttp().getRequest(); + this.logger.warn(`Throttling Salesforce-backed ${this.operationType} due to degraded state`, { + path: request?.originalUrl ?? request?.url, + reason: state.reason, + cooldownExpiresAt: state.cooldownExpiresAt, + usagePercent: state.usagePercent, + }); + + throw new HttpException(this.userMessage, HttpStatus.TOO_MANY_REQUESTS); + } +} diff --git a/apps/bff/src/integrations/salesforce/guards/salesforce-write-throttle.guard.ts b/apps/bff/src/integrations/salesforce/guards/salesforce-write-throttle.guard.ts index c4fa416b..26c71c09 100644 --- a/apps/bff/src/integrations/salesforce/guards/salesforce-write-throttle.guard.ts +++ b/apps/bff/src/integrations/salesforce/guards/salesforce-write-throttle.guard.ts @@ -1,33 +1,9 @@ -import { Inject, Injectable, HttpException, HttpStatus } from "@nestjs/common"; -import type { CanActivate, ExecutionContext } from "@nestjs/common"; -import type { Request } from "express"; -import { Logger } from "nestjs-pino"; -import { SalesforceRequestQueueService } from "@bff/infra/queue/services/salesforce-request-queue.service.js"; +import { Injectable } from "@nestjs/common"; +import { SalesforceThrottleBaseGuard } from "./salesforce-throttle-base.guard.js"; @Injectable() -export class SalesforceWriteThrottleGuard implements CanActivate { - constructor( - private readonly queue: SalesforceRequestQueueService, - @Inject(Logger) private readonly logger: Logger - ) {} - - canActivate(context: ExecutionContext): boolean { - const state = this.queue.getDegradationState(); - if (!state.degraded) { - return true; - } - - const request = context.switchToHttp().getRequest(); - this.logger.warn("Throttling Salesforce-backed write due to degraded state", { - path: request?.originalUrl ?? request?.url, - reason: state.reason, - cooldownExpiresAt: state.cooldownExpiresAt, - usagePercent: state.usagePercent, - }); - - throw new HttpException( - "We're processing a high volume of requests right now. Please retry shortly.", - HttpStatus.TOO_MANY_REQUESTS - ); - } +export class SalesforceWriteThrottleGuard extends SalesforceThrottleBaseGuard { + protected readonly operationType = "write"; + protected readonly userMessage = + "We're processing a high volume of requests right now. Please retry shortly."; }