1+ import { Ratelimit } from "@upstash/ratelimit" ;
12import { createHash } from "node:crypto" ;
23import { env } from "~/env.server" ;
3- import { createRedisClient } from "~/redis.server" ;
44import { getCurrentPlan } from "~/services/platform.v3.server" ;
55import {
66 RateLimiterConfig ,
7+ createLimiterFromConfig ,
78 type RateLimitTokenBucketConfig ,
89} from "~/services/authorizationRateLimitMiddleware.server" ;
9- import type { Duration } from "~/services/rateLimiter.server" ;
10+ import { createRedisRateLimitClient , type Duration } from "~/services/rateLimiter.server" ;
1011import { BasePresenter } from "./basePresenter.server" ;
1112import { singleton } from "~/utils/singleton" ;
1213import { logger } from "~/services/logger.server" ;
1314
1415// Create a singleton Redis client for rate limit queries
15- const rateLimitRedis = singleton ( "rateLimitQueryRedis " , ( ) =>
16- createRedisClient ( "trigger:rateLimitQuery" , {
16+ const rateLimitRedisClient = singleton ( "rateLimitQueryRedisClient " , ( ) =>
17+ createRedisRateLimitClient ( {
1718 port : env . RATE_LIMIT_REDIS_PORT ,
1819 host : env . RATE_LIMIT_REDIS_HOST ,
1920 username : env . RATE_LIMIT_REDIS_USERNAME ,
@@ -391,11 +392,8 @@ function resolveBatchConcurrencyConfig(batchConcurrencyConfig?: unknown): {
391392}
392393
393394/**
394- * Query Redis for the current remaining tokens for a rate limiter.
395- * The @upstash/ratelimit library stores token bucket state in Redis.
396- * Key format: ratelimit:{prefix}:{hashedIdentifier}
397- *
398- * For token bucket, the value is stored as: "tokens:lastRefillTime"
395+ * Query the current remaining tokens for a rate limiter using the Upstash getRemaining method.
396+ * This uses the same configuration and hashing logic as the rate limit middleware.
399397 */
400398async function getRateLimitRemainingTokens (
401399 keyPrefix : string ,
@@ -409,47 +407,20 @@ async function getRateLimitRemainingTokens(
409407 hash . update ( authorizationValue ) ;
410408 const hashedKey = hash . digest ( "hex" ) ;
411409
412- const redis = rateLimitRedis ;
413- const redisKey = `ratelimit:${ keyPrefix } :${ hashedKey } ` ;
414-
415- // Get the stored value from Redis
416- const value = await redis . get ( redisKey ) ;
417-
418- if ( ! value ) {
419- // No rate limit data yet - return max tokens (bucket is full)
420- if ( config . type === "tokenBucket" ) {
421- return config . maxTokens ;
422- } else if ( config . type === "fixedWindow" || config . type === "slidingWindow" ) {
423- return config . tokens ;
424- }
425- return null ;
426- }
427-
428- // For token bucket, the @upstash/ratelimit library stores: "tokens:timestamp"
429- // Parse the value to get remaining tokens
430- if ( typeof value === "string" ) {
431- const parts = value . split ( ":" ) ;
432- if ( parts . length >= 1 ) {
433- const tokens = parseInt ( parts [ 0 ] , 10 ) ;
434- if ( ! isNaN ( tokens ) ) {
435- // For token bucket, we need to calculate current tokens based on refill
436- if ( config . type === "tokenBucket" && parts . length >= 2 ) {
437- const lastRefillTime = parseInt ( parts [ 1 ] , 10 ) ;
438- if ( ! isNaN ( lastRefillTime ) ) {
439- const now = Date . now ( ) ;
440- const elapsed = now - lastRefillTime ;
441- const intervalMs = durationToMs ( config . interval ) ;
442- const tokensToAdd = Math . floor ( elapsed / intervalMs ) * config . refillRate ;
443- const currentTokens = Math . min ( tokens + tokensToAdd , config . maxTokens ) ;
444- return Math . max ( 0 , currentTokens ) ;
445- }
446- }
447- return Math . max ( 0 , tokens ) ;
448- }
449- }
450- }
410+ // Create a Ratelimit instance with the same configuration
411+ const limiter = createLimiterFromConfig ( config ) ;
412+ const ratelimit = new Ratelimit ( {
413+ redis : rateLimitRedisClient ,
414+ limiter,
415+ ephemeralCache : new Map ( ) ,
416+ analytics : false ,
417+ prefix : `ratelimit:${ keyPrefix } ` ,
418+ } ) ;
451419
452- return null ;
420+ // Use the getRemaining method to get the current remaining tokens
421+ // getRemaining returns a Promise<number>
422+ const remaining = await ratelimit . getRemaining ( hashedKey ) ;
423+ return remaining ;
453424 } catch ( error ) {
454425 logger . warn ( "Failed to get rate limit remaining tokens" , {
455426 keyPrefix,
@@ -458,29 +429,3 @@ async function getRateLimitRemainingTokens(
458429 return null ;
459430 }
460431}
461-
462- /**
463- * Convert a duration string (e.g., "1s", "10s", "1m") to milliseconds
464- */
465- function durationToMs ( duration : Duration ) : number {
466- const match = duration . match ( / ^ ( \d + ) ( m s | s | m | h | d ) $ / ) ;
467- if ( ! match ) return 1000 ; // default to 1 second
468-
469- const value = parseInt ( match [ 1 ] , 10 ) ;
470- const unit = match [ 2 ] ;
471-
472- switch ( unit ) {
473- case "ms" :
474- return value ;
475- case "s" :
476- return value * 1000 ;
477- case "m" :
478- return value * 60 * 1000 ;
479- case "h" :
480- return value * 60 * 60 * 1000 ;
481- case "d" :
482- return value * 24 * 60 * 60 * 1000 ;
483- default :
484- return 1000 ;
485- }
486- }
0 commit comments