Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 2 additions & 4 deletions apps/sim/lib/core/execution-limits/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ export function getExecutionTimeout(
type: 'sync' | 'async' = 'sync'
): number {
if (!isBillingEnabled) {
return EXECUTION_TIMEOUTS.enterprise[type]
return EXECUTION_TIMEOUTS.free[type]
}
return EXECUTION_TIMEOUTS[plan || 'free'][type]
}
Expand All @@ -74,9 +74,7 @@ export function getMaxExecutionTimeout(): number {
return EXECUTION_TIMEOUTS.enterprise.async
}

export const DEFAULT_EXECUTION_TIMEOUT_MS = isBillingEnabled
? EXECUTION_TIMEOUTS.free.sync
: EXECUTION_TIMEOUTS.enterprise.sync
export const DEFAULT_EXECUTION_TIMEOUT_MS = EXECUTION_TIMEOUTS.free.sync

export function isTimeoutError(error: unknown): boolean {
if (!error) return false
Expand Down
1 change: 1 addition & 0 deletions apps/sim/lib/core/rate-limiter/rate-limiter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type { ConsumeResult, RateLimitStorageAdapter, TokenStatus } from './stor
import { MANUAL_EXECUTION_LIMIT, RATE_LIMITS, RateLimitError } from './types'

vi.mock('@sim/logger', () => loggerMock)
vi.mock('@/lib/core/config/feature-flags', () => ({ isBillingEnabled: true }))

interface MockAdapter {
consumeTokens: Mock
Expand Down
45 changes: 11 additions & 34 deletions apps/sim/lib/core/rate-limiter/rate-limiter.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,9 @@
import { createLogger } from '@sim/logger'
import { createStorageAdapter, type RateLimitStorageAdapter } from './storage'
import {
createStorageAdapter,
type RateLimitStorageAdapter,
type TokenBucketConfig,
} from './storage'
import {
getRateLimit,
MANUAL_EXECUTION_LIMIT,
RATE_LIMIT_WINDOW_MS,
RATE_LIMITS,
type RateLimitCounterType,
type SubscriptionPlan,
type TriggerType,
Expand Down Expand Up @@ -57,21 +53,6 @@ export class RateLimiter {
return isAsync ? 'async' : 'sync'
}

private getBucketConfig(
plan: SubscriptionPlan,
counterType: RateLimitCounterType
): TokenBucketConfig {
const config = RATE_LIMITS[plan]
switch (counterType) {
case 'api-endpoint':
return config.apiEndpoint
case 'async':
return config.async
case 'sync':
return config.sync
}
}

private buildStorageKey(rateLimitKey: string, counterType: RateLimitCounterType): string {
return `${rateLimitKey}:${counterType}`
}
Expand All @@ -84,15 +65,6 @@ export class RateLimiter {
}
}

private createUnlimitedStatus(config: TokenBucketConfig): RateLimitStatus {
return {
requestsPerMinute: MANUAL_EXECUTION_LIMIT,
maxBurst: MANUAL_EXECUTION_LIMIT,
remaining: MANUAL_EXECUTION_LIMIT,
resetAt: new Date(Date.now() + config.refillIntervalMs),
}
}

async checkRateLimitWithSubscription(
userId: string,
subscription: SubscriptionInfo | null,
Expand All @@ -107,7 +79,7 @@ export class RateLimiter {
const plan = (subscription?.plan || 'free') as SubscriptionPlan
const rateLimitKey = this.getRateLimitKey(userId, subscription)
const counterType = this.getCounterType(triggerType, isAsync)
const config = this.getBucketConfig(plan, counterType)
const config = getRateLimit(plan, counterType)
const storageKey = this.buildStorageKey(rateLimitKey, counterType)

const result = await this.storage.consumeTokens(storageKey, 1, config)
Expand Down Expand Up @@ -152,10 +124,15 @@ export class RateLimiter {
try {
const plan = (subscription?.plan || 'free') as SubscriptionPlan
const counterType = this.getCounterType(triggerType, isAsync)
const config = this.getBucketConfig(plan, counterType)
const config = getRateLimit(plan, counterType)

if (triggerType === 'manual') {
return this.createUnlimitedStatus(config)
return {
requestsPerMinute: MANUAL_EXECUTION_LIMIT,
maxBurst: MANUAL_EXECUTION_LIMIT,
remaining: MANUAL_EXECUTION_LIMIT,
resetAt: new Date(Date.now() + config.refillIntervalMs),
}
}

const rateLimitKey = this.getRateLimitKey(userId, subscription)
Expand All @@ -178,7 +155,7 @@ export class RateLimiter {
})
const plan = (subscription?.plan || 'free') as SubscriptionPlan
const counterType = this.getCounterType(triggerType, isAsync)
const config = this.getBucketConfig(plan, counterType)
const config = getRateLimit(plan, counterType)
return {
requestsPerMinute: config.refillRate,
maxBurst: config.maxTokens,
Expand Down
73 changes: 61 additions & 12 deletions apps/sim/lib/core/rate-limiter/types.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import { env } from '@/lib/core/config/env'
import { isBillingEnabled } from '@/lib/core/config/feature-flags'
import type { CoreTriggerType } from '@/stores/logs/filters/types'
import type { TokenBucketConfig } from './storage'

export type TriggerType = CoreTriggerType | 'form' | 'api-endpoint'

export type RateLimitCounterType = 'sync' | 'async' | 'api-endpoint'

type RateLimitConfigKey = 'sync' | 'async' | 'apiEndpoint'

export type SubscriptionPlan = 'free' | 'pro' | 'team' | 'enterprise'

export interface RateLimitConfig {
Expand All @@ -18,6 +21,17 @@ export const RATE_LIMIT_WINDOW_MS = Number.parseInt(env.RATE_LIMIT_WINDOW_MS) ||

export const MANUAL_EXECUTION_LIMIT = Number.parseInt(env.MANUAL_EXECUTION_LIMIT) || 999999

const DEFAULT_RATE_LIMITS = {
free: { sync: 50, async: 200, apiEndpoint: 30 },
pro: { sync: 150, async: 1000, apiEndpoint: 100 },
team: { sync: 300, async: 2500, apiEndpoint: 200 },
enterprise: { sync: 600, async: 5000, apiEndpoint: 500 },
} as const

function toConfigKey(type: RateLimitCounterType): RateLimitConfigKey {
return type === 'api-endpoint' ? 'apiEndpoint' : type
}

function createBucketConfig(ratePerMinute: number, burstMultiplier = 2): TokenBucketConfig {
return {
maxTokens: ratePerMinute * burstMultiplier,
Expand All @@ -26,29 +40,64 @@ function createBucketConfig(ratePerMinute: number, burstMultiplier = 2): TokenBu
}
}

function getRateLimitForPlan(plan: SubscriptionPlan, type: RateLimitConfigKey): TokenBucketConfig {
const envVarMap: Record<SubscriptionPlan, Record<RateLimitConfigKey, string | undefined>> = {
free: {
sync: env.RATE_LIMIT_FREE_SYNC,
async: env.RATE_LIMIT_FREE_ASYNC,
apiEndpoint: undefined,
},
pro: { sync: env.RATE_LIMIT_PRO_SYNC, async: env.RATE_LIMIT_PRO_ASYNC, apiEndpoint: undefined },
team: {
sync: env.RATE_LIMIT_TEAM_SYNC,
async: env.RATE_LIMIT_TEAM_ASYNC,
apiEndpoint: undefined,
},
enterprise: {
sync: env.RATE_LIMIT_ENTERPRISE_SYNC,
async: env.RATE_LIMIT_ENTERPRISE_ASYNC,
apiEndpoint: undefined,
},
}

const rate = Number.parseInt(envVarMap[plan][type] || '') || DEFAULT_RATE_LIMITS[plan][type]
return createBucketConfig(rate)
}

export const RATE_LIMITS: Record<SubscriptionPlan, RateLimitConfig> = {
free: {
sync: createBucketConfig(Number.parseInt(env.RATE_LIMIT_FREE_SYNC) || 50),
async: createBucketConfig(Number.parseInt(env.RATE_LIMIT_FREE_ASYNC) || 200),
apiEndpoint: createBucketConfig(30),
sync: getRateLimitForPlan('free', 'sync'),
async: getRateLimitForPlan('free', 'async'),
apiEndpoint: getRateLimitForPlan('free', 'apiEndpoint'),
},
pro: {
sync: createBucketConfig(Number.parseInt(env.RATE_LIMIT_PRO_SYNC) || 150),
async: createBucketConfig(Number.parseInt(env.RATE_LIMIT_PRO_ASYNC) || 1000),
apiEndpoint: createBucketConfig(100),
sync: getRateLimitForPlan('pro', 'sync'),
async: getRateLimitForPlan('pro', 'async'),
apiEndpoint: getRateLimitForPlan('pro', 'apiEndpoint'),
},
team: {
sync: createBucketConfig(Number.parseInt(env.RATE_LIMIT_TEAM_SYNC) || 300),
async: createBucketConfig(Number.parseInt(env.RATE_LIMIT_TEAM_ASYNC) || 2500),
apiEndpoint: createBucketConfig(200),
sync: getRateLimitForPlan('team', 'sync'),
async: getRateLimitForPlan('team', 'async'),
apiEndpoint: getRateLimitForPlan('team', 'apiEndpoint'),
},
enterprise: {
sync: createBucketConfig(Number.parseInt(env.RATE_LIMIT_ENTERPRISE_SYNC) || 600),
async: createBucketConfig(Number.parseInt(env.RATE_LIMIT_ENTERPRISE_ASYNC) || 5000),
apiEndpoint: createBucketConfig(500),
sync: getRateLimitForPlan('enterprise', 'sync'),
async: getRateLimitForPlan('enterprise', 'async'),
apiEndpoint: getRateLimitForPlan('enterprise', 'apiEndpoint'),
},
}

export function getRateLimit(
plan: SubscriptionPlan | undefined,
type: RateLimitCounterType
): TokenBucketConfig {
const key = toConfigKey(type)
if (!isBillingEnabled) {
return RATE_LIMITS.free[key]
}
return RATE_LIMITS[plan || 'free'][key]
}

export class RateLimitError extends Error {
statusCode: number
constructor(message: string, statusCode = 429) {
Expand Down
17 changes: 9 additions & 8 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
"glob": "13.0.0",
"husky": "9.1.7",
"lint-staged": "16.0.0",
"turbo": "2.8.0"
"turbo": "2.8.3"
},
"lint-staged": {
"*.{js,jsx,ts,tsx,json,css,scss}": [
Expand Down