Skip to content

Commit bd74089

Browse files
committed
perf(router): lazy-load action limiter
1 parent 23ced38 commit bd74089

2 files changed

Lines changed: 32 additions & 12 deletions

File tree

storage/framework/core/router/src/rate-limit.ts

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,7 @@
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'
2019
import { getCurrentRequest } from './request-context'
2120

2221
const PERIOD_SECONDS = {
@@ -38,11 +37,17 @@ type Period = keyof typeof PERIOD_SECONDS
3837
* RateLimiter instance).
3938
*/
4039
const 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
}

storage/framework/core/router/tests/import-graph.test.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,5 +48,12 @@ describe('router import graph', () => {
4848
const eagerRateLimiterImports = routerEntry?.[1].imports
4949
.filter(entry => entry.kind !== 'dynamic-import' && entry.path.endsWith('router/src/rate-limit.ts')) ?? []
5050
expect(eagerRateLimiterImports).toEqual([])
51+
52+
const rateLimitEntry = Object.entries(result.metafile?.inputs ?? {})
53+
.find(([source]) => source.endsWith('router/src/rate-limit.ts'))
54+
const eagerActionLimiterDependencies = rateLimitEntry?.[1].imports
55+
.filter(entry => entry.kind !== 'dynamic-import'
56+
&& (entry.path.includes('ts-rate-limiter') || entry.path.endsWith('error-handling/src/http.ts'))) ?? []
57+
expect(eagerActionLimiterDependencies).toEqual([])
5158
})
5259
})

0 commit comments

Comments
 (0)