1515 * action handler can invoke conditionally. This module provides that.
1616 */
1717
18- import { HttpError } from '@stacksjs/error-handling/http'
19- import { RateLimitError , RateLimiter , defaultIdentity } from 'ts-rate-limiter'
18+ import type { RateLimiter } from 'ts-rate-limiter'
2019import { getCurrentRequest } from './request-context'
2120
2221const PERIOD_SECONDS = {
@@ -38,11 +37,17 @@ type Period = keyof typeof PERIOD_SECONDS
3837 * RateLimiter instance).
3938 */
4039const limiterCache = new Map < string , RateLimiter > ( )
40+ let limiterModulePromise : Promise < typeof import ( 'ts-rate-limiter' ) > | undefined
4141
42- function getLimiter ( max : number , windowMs : number ) : RateLimiter {
42+ function loadLimiterModule ( ) : Promise < typeof import ( 'ts-rate-limiter' ) > {
43+ return limiterModulePromise ??= import ( 'ts-rate-limiter' )
44+ }
45+
46+ async function getLimiter ( max : number , windowMs : number ) : Promise < RateLimiter > {
4347 const cacheKey = `${ windowMs } :${ max } `
4448 let limiter = limiterCache . get ( cacheKey )
4549 if ( ! limiter ) {
50+ const { RateLimiter } = await loadLimiterModule ( )
4651 limiter = new RateLimiter ( {
4752 windowMs,
4853 maxRequests : max ,
@@ -62,9 +67,10 @@ function getLimiter(max: number, windowMs: number): RateLimiter {
6267 * `defaultIdentity(req)` from `ts-rate-limiter` (auth user → token →
6368 * IP → 'anon'); callers can override via `options.identity`.
6469 */
65- function resolveIdentity ( explicit ?: string ) : string {
70+ async function resolveIdentity ( explicit ?: string ) : Promise < string > {
6671 if ( explicit !== undefined ) return explicit
6772 const req = getCurrentRequest ( ) as Request | undefined
73+ const { defaultIdentity } = await loadLimiterModule ( )
6874 return req ? defaultIdentity ( req ) : 'anon'
6975}
7076
@@ -88,16 +94,19 @@ export function rateLimit(
8894 /** Run with a numeric ttl in seconds. */
8995 over : ( ttlSeconds : number ) => Promise < void >
9096 } {
91- const id = resolveIdentity ( options . identity )
92- const bucketKey = `${ key } :${ id } `
93-
9497 const run = async ( windowMs : number ) : Promise < void > => {
95- const limiter = getLimiter ( max , windowMs )
98+ const [ id , limiter ] = await Promise . all ( [
99+ resolveIdentity ( options . identity ) ,
100+ getLimiter ( max , windowMs ) ,
101+ ] )
102+ const bucketKey = `${ key } :${ id } `
96103 try {
97104 await limiter . enforce ( bucketKey )
98105 }
99106 catch ( err ) {
107+ const { RateLimitError } = await loadLimiterModule ( )
100108 if ( err instanceof RateLimitError ) {
109+ const { HttpError } = await import ( '@stacksjs/error-handling/http' )
101110 throw Object . assign (
102111 new HttpError ( 429 , 'Too many requests' , {
103112 key,
@@ -138,9 +147,11 @@ export async function rateLimitStatus(
138147 windowSeconds : number ,
139148 options : { identity ?: string } = { } ,
140149) : Promise < { count : number , limit : number , remaining : number } | null > {
141- const id = resolveIdentity ( options . identity )
150+ const [ id , limiter ] = await Promise . all ( [
151+ resolveIdentity ( options . identity ) ,
152+ getLimiter ( max , windowSeconds * 1000 ) ,
153+ ] )
142154 const bucketKey = `${ key } :${ id } `
143- const limiter = getLimiter ( max , windowSeconds * 1000 )
144155 const result = await limiter . peek ( bucketKey )
145156 if ( ! result ) return null
146157 return {
@@ -160,8 +171,10 @@ export async function clearRateLimit(
160171 windowSeconds : number ,
161172 options : { identity ?: string } = { } ,
162173) : Promise < void > {
163- const id = resolveIdentity ( options . identity )
174+ const [ id , limiter ] = await Promise . all ( [
175+ resolveIdentity ( options . identity ) ,
176+ getLimiter ( max , windowSeconds * 1000 ) ,
177+ ] )
164178 const bucketKey = `${ key } :${ id } `
165- const limiter = getLimiter ( max , windowSeconds * 1000 )
166179 await limiter . reset ( bucketKey )
167180}
0 commit comments