Skip to content

Commit

Permalink
feat: debug
Browse files Browse the repository at this point in the history
  • Loading branch information
rayriffy committed Apr 20, 2024
1 parent 098e4c7 commit d4d7a62
Show file tree
Hide file tree
Showing 5 changed files with 46 additions and 11 deletions.
5 changes: 5 additions & 0 deletions .changeset/empty-parents-flash.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'elysia-rate-limit': patch
---

added debug logs
25 changes: 23 additions & 2 deletions src/services/defaultContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@ import { LRUCache } from 'lru-cache'

import type { Options } from '../@types/Options'
import type { Context } from '../@types/Context'
import { logger } from './logger'

interface Item {
count: number
nextReset: Date
}

export class DefaultContext implements Context {
private readonly id: string = (Math.random() + 1).toString(36).substring(7)
private readonly maxSize: number
private store!: LRUCache<string, Item>
private duration!: number
Expand All @@ -18,6 +20,8 @@ export class DefaultContext implements Context {
}

public init (options: Options) {
logger(`context:${this.id}`, 'initialized with maxSize: %d, and expire duration of %d seconds', this.maxSize, options.duration / 1000)

this.duration = options.duration
this.store = new LRUCache<string, Item>({
max: this.maxSize,
Expand All @@ -29,14 +33,25 @@ export class DefaultContext implements Context {
let item = this.store.get(key)

// if item is not found or expired, then issue a new one
if (item === undefined || item.nextReset < now)
if (item === undefined || item.nextReset < now) {
logger(
`context:${this.id}`,
'created new item for key: %s (reason: %s)',
key,
item === undefined ? 'not found' : 'expired'
)

item = {
count: 1,
nextReset: new Date(now.getTime() + this.duration),
}
}
// otherwise, increment the count
else
else {
logger(`context:${this.id}`, 'incremented count for key: %s', key)

item.count++
}

// update the store
this.store.set(key, item)
Expand All @@ -49,6 +64,8 @@ export class DefaultContext implements Context {

// perform actions only if an item is found
if (item !== undefined) {
logger(`context:${this.id}`, 'decremented count for key: %s', key)

// decrement the count by 1
item.count--

Expand All @@ -58,13 +75,17 @@ export class DefaultContext implements Context {
}

public async reset(key?: string) {
logger(`context:${this.id}`, 'resetting target %s', key ?? 'all')

if (typeof key === 'string')
this.store.delete(key)
else
this.store.clear()
}

public kill() {
logger(`context:${this.id}`, 'clearing the store')

this.store.clear()
}
}
4 changes: 4 additions & 0 deletions src/services/defaultKeyGenerator.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import { logger } from './logger'

import type { Options } from '../@types/Options'

export const defaultKeyGenerator: Options['generator'] = (request, server): string => {
const clientAddress = server?.requestIP(request)?.address

logger('generator', 'clientAddress: %s', clientAddress)

if (clientAddress === undefined) {
let reason: string

Expand Down
4 changes: 4 additions & 0 deletions src/services/logger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import debug from 'debug'

export const logger = (unit: string, formatter: any, ...params: any[]) =>
debug('elysia-rate-limit:' + unit)(formatter, ...params)
19 changes: 10 additions & 9 deletions src/services/plugin.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
import type Elysia from 'elysia'
import debug from 'debug'

import { defaultOptions } from '../constants/defaultOptions'

import type { Options } from '../@types/Options'

const logger = (unit: string, formatter: any, ...params: any[]) =>
debug('elysia-rate-limit:' + unit)(formatter, ...params)
import { logger } from './logger'

export const plugin = (userOptions?: Partial<Options>) => {
const options: Options = {
Expand Down Expand Up @@ -40,8 +37,6 @@ export const plugin = (userOptions?: Partial<Options>) => {
if (options.skip.length < 2)
clientKey = await options.generator(request, app.server, rest)

logger('generator', 'generated key is %s', clientKey)

const { count, nextReset } = await options.context.increment(clientKey!)

const payload = {
Expand All @@ -52,32 +47,38 @@ export const plugin = (userOptions?: Partial<Options>) => {
}

// set standard headers
const reset = Math.max(0, Math.ceil((nextReset.getTime() - Date.now()) / 1000))
set.headers['RateLimit-Limit'] = String(options.max)
set.headers['RateLimit-Remaining'] = String(payload.remaining)
set.headers['RateLimit-Reset'] = String(
Math.max(0, Math.ceil((nextReset.getTime() - Date.now()) / 1000))
)
set.headers['RateLimit-Reset'] = String(reset)

// reject if limit were reached
if (payload.current >= payload.limit + 1) {
logger('plugin', 'rate limit exceeded for clientKey: %s (resetting in %d seconds)', clientKey, reset)

set.headers['Retry-After'] = String(
Math.ceil(options.duration / 1000)
)
set.status = options.responseCode
return options.responseMessage
}

logger('plugin', 'clientKey %s passed through with %d/%d request used (resetting in %d seconds)', clientKey, options.max - payload.remaining, options.max, reset)
}
})

// @ts-expect-error somehow qi is being sent from elysia, but there's no type declaration for it
app.onError({ as: options.scoping }, async ({ set, request, query, path, store, cookie, error, body, params, headers, qi, code, ...rest }) => {
if (!options.countFailedRequest) {
const clientKey = await options.generator(request, app.server, rest)

logger('plugin', 'request failed for clientKey: %s, refunding', clientKey)
await options.context.decrement(clientKey)
}
})

app.onStop(async () => {
logger('plugin', 'kill signal received')
await options.context.kill()
})

Expand Down

0 comments on commit d4d7a62

Please sign in to comment.